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,2 @@
# output 节点不应被忽略
!output

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 { BatchPort } from './loop-port';

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Port } from '@/components/node-render/node-render-new/fields/port';
export const BatchPort = () => (
<>
<Port
id={'batch-output-to-function'}
type="output"
style={{
position: 'absolute',
width: 20,
height: 20,
right: 'unset',
top: 'unset',
bottom: 0,
left: '50%',
transform: 'translate(-50%, 50%)',
}}
/>
<Port
id={'batch-output'}
type="output"
style={{
position: 'absolute',
right: '0',
}}
/>
</>
);

View File

@@ -0,0 +1,129 @@
/*
* 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 {
WorkflowNodeEntity,
WorkflowSubCanvas,
} from '@flowgram-adapter/free-layout-editor';
import {
FlowNodeBaseType,
FlowNodeTransformData,
} from '@flowgram-adapter/free-layout-editor';
import { FlowRendererKey } from '@flowgram-adapter/free-layout-editor';
import {
type IPoint,
type PaddingSchema,
type PositionSchema,
} from '@flowgram-adapter/common';
import type { WorkflowNodeJSON } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { BatchFunctionSize } from '../constants';
import { getBatchID } from './relation';
export const createBatchFunctionJSON = (
id: string,
position: IPoint,
): WorkflowNodeJSON => ({
id,
type: FlowNodeBaseType.SUB_CANVAS,
data: {},
meta: {
isContainer: true,
position,
nodeDTOType: FlowNodeBaseType.SUB_CANVAS,
useDynamicPort: true,
renderKey: FlowRendererKey.SUB_CANVAS,
size: {
width: BatchFunctionSize.width,
height: BatchFunctionSize.height,
},
defaultPorts: [
{ type: 'input', portID: 'batch-function-input', disabled: true },
{ type: 'input', portID: 'batch-function-inline-input' },
{ type: 'output', portID: 'batch-function-inline-output' },
],
padding: (transform: FlowNodeTransformData): PaddingSchema => ({
top: 100,
bottom: 60,
left: 100,
right: 100,
}),
selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {
if (!mousePos) {
return true;
}
const transform = node.getData<FlowNodeTransformData>(
FlowNodeTransformData,
);
// 鼠标开始时所在位置不包括当前节点时才可选中
return !transform.bounds.contains(mousePos.x, mousePos.y);
},
renderSubCanvas: () => ({
title: I18n.t('workflow_batch_canvas_title'),
tooltip: I18n.t('workflow_batch_canvas_tooltips'),
style: {
minWidth: BatchFunctionSize.width,
minHeight: BatchFunctionSize.height,
},
renderPorts: [
{
id: 'batch-function-input',
type: 'input',
style: {
position: 'absolute',
left: '50%',
top: '0',
},
},
{
id: 'batch-function-inline-input',
type: 'input',
style: {
position: 'absolute',
right: '0',
top: '50%',
transform: 'translateY(20px)',
},
},
{
id: 'batch-function-inline-output',
type: 'output',
style: {
position: 'absolute',
left: '0',
top: '50%',
transform: 'translateY(20px)',
},
},
],
}),
subCanvas: (node: WorkflowNodeEntity): WorkflowSubCanvas | undefined => {
const canvasNode = node;
const parentNodeID = getBatchID(canvasNode.id);
const parentNode = node.document.getNode(parentNodeID);
if (!parentNode) {
return undefined;
}
const subCanvas: WorkflowSubCanvas = {
isCanvas: true,
parentNode,
canvasNode,
};
return subCanvas;
},
},
});

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 type { WorkflowDocument } from '@flowgram-adapter/free-layout-editor';
import { delay } from '@flowgram-adapter/common';
/** 生成连线 */
export const createBatchFunctionLines = async (params: {
document: WorkflowDocument;
batchId: string;
batchFunctionId: string;
}) => {
await delay(30); // 等待节点创建完毕
const { document, batchId, batchFunctionId } = params;
document.linesManager.createLine({
from: batchId,
to: batchFunctionId,
fromPort: 'batch-output-to-function',
toPort: 'batch-function-input',
});
};

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 { WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { type NodeData, WorkflowNodeData } from '@coze-workflow/nodes';
import type { BasicStandardNodeTypes } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
/** 同步节点模版数据 */
export const createBatchFunctionTemplateData = (
batchNode: WorkflowNodeEntity,
batchFunctionNode: WorkflowNodeEntity,
) => {
const batchNodeDataEntity =
batchNode.getData<WorkflowNodeData>(WorkflowNodeData);
const batchFunctionNodeDataEntity =
batchFunctionNode.getData<WorkflowNodeData>(WorkflowNodeData);
const batchNodeData = batchNodeDataEntity.getNodeData<keyof NodeData>();
if (!batchNodeData) {
return;
}
batchFunctionNodeDataEntity.setNodeData<BasicStandardNodeTypes>({
title: I18n.t('workflow_batch_canvas_title'),
description: I18n.t('workflow_batch_canvas_tooltips'),
icon: batchNodeData.icon,
mainColor: batchNodeData.mainColor,
});
};

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {
WorkflowDocument,
WorkflowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { type IPoint } from '@flowgram-adapter/common';
import type { WorkflowNodeJSON } from '@coze-workflow/base';
import { BatchFunctionIDPrefix } from './relation';
import { createBatchFunctionTemplateData } from './create-batch-function-template-data';
import { createBatchFunctionLines } from './create-batch-function-lines';
import { createBatchFunctionJSON } from './create-batch-function-json';
/** 创建 Batch 循环体节点 */
export const createBatchFunction = async (
batchNode: WorkflowNodeEntity,
batchJson: WorkflowNodeJSON,
) => {
const document = batchNode.document as WorkflowDocument;
const id = `${BatchFunctionIDPrefix}${batchNode.id}`;
const batchPosition: IPoint = {
x: batchJson.meta?.position?.x || 0,
y: batchJson.meta?.position?.y || 0,
};
const offset: IPoint = {
x: 0,
y: 200,
};
const position = {
x: batchPosition.x + offset.x,
y: batchPosition.y + offset.y,
};
const batchFunctionJSON = createBatchFunctionJSON(id, position);
const batchFunctionNode =
await document.createWorkflowNode(batchFunctionJSON);
createBatchFunctionTemplateData(batchNode, batchFunctionNode);
createBatchFunctionLines({
document,
batchId: batchNode.id,
batchFunctionId: batchFunctionNode.id,
});
};

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.
*/
export { createBatchFunctionJSON } from './create-batch-function-json';
export { createBatchFunctionLines } from './create-batch-function-lines';
export { createBatchFunctionTemplateData } from './create-batch-function-template-data';
export { createBatchFunction } from './create-batch-function';
export {
BatchFunctionIDPrefix,
getBatchFunctionID,
getBatchID,
} from './relation';

View File

@@ -0,0 +1,22 @@
/*
* 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/naming-convention*/
export const BatchFunctionIDPrefix = 'BatchFunction_';
export const getBatchFunctionID = (batchID: string) =>
BatchFunctionIDPrefix + batchID;
export const getBatchID = (batchFunctionID: string) =>
batchFunctionID.replace(BatchFunctionIDPrefix, '');

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.
*/
/* eslint-disable @typescript-eslint/naming-convention*/
/* eslint-disable @typescript-eslint/no-shadow */
export const BatchSize = {
width: 360,
height: 139.86,
};
export const BatchFunctionSize = {
width: BatchSize.width,
height: (BatchSize.width * 3) / 5,
};
export const BatchOutputsSuffix = '_list';
export enum BatchPath {
ConcurrentSize = 'inputs.concurrentSize',
BatchSize = 'inputs.batchSize',
Inputs = 'inputs.inputParameters',
Outputs = 'outputs',
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { set } from 'lodash-es';
import { variableUtils } from '@coze-workflow/variable';
import { type NodeDataDTO } from '@coze-workflow/base';
/**
* 节点后端数据 -> 前端表单数据
*/
export const transformOnInit = (formData: any, ctx: any) => {
const inputParameters = formData?.inputs?.inputParameters;
const outputValues = formData?.outputs;
const concurrentSize = formData?.inputs?.concurrentSize;
const batchSize = formData?.inputs?.batchSize;
if (!Array.isArray(inputParameters) || inputParameters?.length === 0) {
set(formData, 'inputs.inputParameters', [{ name: 'input' }]);
}
if (outputValues && Array.isArray(outputValues)) {
outputValues.map((outputValue, index) => {
set(
outputValues,
index,
variableUtils.inputValueToVO(
outputValue,
ctx.playgroundContext.variableService,
),
);
});
}
if (concurrentSize) {
set(
formData,
'inputs.concurrentSize',
variableUtils.valueExpressionToVO(
concurrentSize,
ctx.playgroundContext.variableService,
),
);
}
if (batchSize) {
set(
formData,
'inputs.batchSize',
variableUtils.valueExpressionToVO(
batchSize,
ctx.playgroundContext.variableService,
),
);
}
return formData;
};
/**
* 前端表单数据 -> 节点后端数据
* @param value
* @returns
*/
export const transformOnSubmit = (formData: any, ctx: any): NodeDataDTO => {
const outputValues = formData?.outputs;
const concurrentSize = formData?.inputs?.concurrentSize;
const batchSize = formData?.inputs?.batchSize;
if (outputValues && Array.isArray(outputValues)) {
outputValues.map((outputValue, index) => {
const dto = variableUtils.inputValueToDTO(
outputValue,
ctx.playgroundContext.variableService,
{ node: ctx.node },
);
// 定制逻辑:如果选择了循环体内的变量,则输出变量的类型套一层 list
if (
outputValue?.input?.content?.keyPath?.[0] !== ctx.node.id &&
dto?.input
) {
set(dto, 'input.schema', {
type: dto.input?.type,
schema: dto.input?.schema,
});
set(dto, 'input.type', 'list');
}
set(outputValues, index, dto);
});
set(formData, 'outputs', outputValues.filter(Boolean));
}
if (concurrentSize) {
set(
formData,
'inputs.concurrentSize',
variableUtils.valueExpressionToDTO(
concurrentSize,
ctx.playgroundContext.variableService,
{ node: ctx.node },
),
);
}
if (batchSize) {
set(
formData,
'inputs.batchSize',
variableUtils.valueExpressionToDTO(
batchSize,
ctx.playgroundContext.variableService,
{ node: ctx.node },
),
);
}
return formData;
};

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 {
useNodeTestId,
type ValueExpression,
ValueExpressionType,
ViewVariableType,
} from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { ValueExpressionInput } from '@/form-extensions/components/value-expression-input';
import { FormItem } from '@/form-extensions/components/form-item';
import { useField, withField } from '@/form';
interface BatchConcurrentSizeFieldProps {
title?: string;
tooltip?: string;
testId?: string;
}
export const BatchConcurrentSizeField = withField<
BatchConcurrentSizeFieldProps,
ValueExpression
>(
({
title = I18n.t('workflow_maximum_parallel_runs'),
tooltip = I18n.t('workflow_maximum_parallel_runs_tips'),
testId,
}) => {
const { name, value, onChange, readonly } = useField<ValueExpression>();
const { getNodeSetterId } = useNodeTestId();
return (
<FormItem
label={title}
tooltip={tooltip}
layout="vertical"
style={{
marginTop: 12,
}}
labelStyle={{
fontSize: 12,
fontWeight: 600,
color: 'var(--coz-fg-secondary, rgba(6, 7, 9, 0.50))',
}}
>
<ValueExpressionInput
value={value}
onChange={onChange}
testId={testId ?? getNodeSetterId(name)}
disabledTypes={ViewVariableType.getComplement([
ViewVariableType.Integer,
])}
readonly={readonly}
inputType={ViewVariableType.Integer}
literalConfig={{
min: 1,
max: 10,
}}
literalStyle={{
width: '100%',
}}
/>
</FormItem>
);
},
{
defaultValue: { type: ValueExpressionType.LITERAL, content: 10 },
},
);

View File

@@ -0,0 +1,45 @@
/*
* 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/consistent-type-assertions */
import { type FC } from 'react';
import { type InputValueVO, ViewVariableType } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { InputsField } from '@/node-registries/common/fields';
import { BatchPath } from '../../constants';
interface BatchInputsFieldProps {
name?: string;
}
export const BatchInputsField: FC<BatchInputsFieldProps> = ({ name }) => (
<InputsField
name={name ?? BatchPath.Inputs}
title={I18n.t('workflow_batch_inputs')}
tooltip={I18n.t('workflow_batch_inputs_tooltips')}
defaultValue={[{ name: 'input' } as InputValueVO]}
nthCannotDeleted={1}
inputProps={{
hideDeleteIcon: true,
disabledTypes: ViewVariableType.getComplement(
ViewVariableType.getAllArrayType(),
),
}}
/>
);

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { type FC } from 'react';
import { type InputValueVO } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { LoopOutputsField } from '@/node-registries/common/fields';
import { BatchOutputsSuffix, BatchPath } from '../../constants';
interface BatchOutputsFieldProps {
name?: string;
title?: string;
tooltip?: string;
}
export const BatchOutputsField: FC<BatchOutputsFieldProps> = ({
name = BatchPath.Outputs,
title = I18n.t('workflow_batch_outputs'),
tooltip = I18n.t('workflow_batch_outputs_tooltips'),
}) => (
<LoopOutputsField
name={name}
title={title}
tooltip={tooltip}
defaultValue={[{ name: 'output' } as InputValueVO]}
nameProps={{
initValidate: true,
suffix: BatchOutputsSuffix,
}}
/>
);

View File

@@ -0,0 +1,81 @@
/*
* 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 {
useNodeTestId,
type ValueExpression,
ValueExpressionType,
ViewVariableType,
} from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { ValueExpressionInput } from '@/form-extensions/components/value-expression-input';
import { FormItem } from '@/form-extensions/components/form-item';
import { useField, withField } from '@/form';
interface BatchSizeFieldProps {
title?: string;
tooltip?: string;
testId?: string;
}
export const BatchSizeField = withField<BatchSizeFieldProps, ValueExpression>(
({
title = I18n.t('workflow_maximum_run_count'),
tooltip = I18n.t('workflow_maximum_run_count_tips'),
testId,
}) => {
const { name, value, onChange, readonly } = useField<ValueExpression>();
const { getNodeSetterId } = useNodeTestId();
return (
<FormItem
label={title}
tooltip={tooltip}
layout="vertical"
style={{
marginTop: 12,
}}
labelStyle={{
fontSize: 12,
fontWeight: 600,
color: 'var(--coz-fg-secondary, rgba(6, 7, 9, 0.50))',
}}
>
<ValueExpressionInput
value={value}
onChange={onChange}
testId={testId ?? getNodeSetterId(name)}
disabledTypes={ViewVariableType.getComplement([
ViewVariableType.Integer,
])}
readonly={readonly}
inputType={ViewVariableType.Integer}
literalConfig={{
min: 1,
max: 200,
}}
literalStyle={{
width: '100%',
}}
/>
</FormItem>
);
},
{
defaultValue: { type: ValueExpressionType.LITERAL, content: 100 },
},
);

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.
*/
export { BatchConcurrentSizeField } from './batch-concurrent-size';
export { BatchInputsField } from './batch-inputs';
export { BatchOutputsField } from './batch-outputs';
export { BatchSizeField } from './batch-size';
export { BatchSettingsSection } from './loop-settings-section';

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { FC, ReactNode } from 'react';
import { useNodeTestId } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { Section } from '@/form';
interface BatchSettingsFieldProps {
title?: string;
tooltip?: string;
testId?: string;
children?: ReactNode | ReactNode[];
}
export const BatchSettingsSection: FC<BatchSettingsFieldProps> = ({
title = I18n.t('workflow_loop_title'),
tooltip,
testId,
children,
}) => {
const { getNodeSetterId } = useNodeTestId();
return (
<Section
title={title}
tooltip={tooltip}
testId={getNodeSetterId(testId ?? '')}
>
{children}
</Section>
);
};

View File

@@ -0,0 +1,67 @@
/*
* 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 {
ValidateTrigger,
type FormMetaV2,
} from '@flowgram-adapter/free-layout-editor';
import { nodeMetaValidate } from '@/nodes-v2/materials/node-meta-validate';
import {
fireNodeTitleChange,
provideLoopInputsVariablesEffect,
provideLoopOutputsVariablesEffect,
} from '@/node-registries/common/effects';
import {
BatchInputNameValidator,
BatchInputValueValidator,
BatchOutputNameValidator,
} from './validators';
import { type FormData } from './types';
import { BatchFormRender } from './form';
import { transformOnInit, transformOnSubmit } from './data-transformer';
import { BatchPath } from './constants';
export const BATCH_FORM_META: FormMetaV2<FormData> = {
// 节点表单渲染
render: () => <BatchFormRender />,
// 验证触发时机
validateTrigger: ValidateTrigger.onChange,
// 验证规则
validate: {
nodeMeta: nodeMetaValidate,
[`${BatchPath.Inputs}.*.name`]: BatchInputNameValidator,
[`${BatchPath.Inputs}.*.input`]: BatchInputValueValidator,
[`${BatchPath.Outputs}.*.name`]: BatchOutputNameValidator,
[`${BatchPath.Outputs}.*.input`]: BatchInputValueValidator,
},
// 副作用管理
effect: {
nodeMeta: fireNodeTitleChange,
inputs: provideLoopInputsVariablesEffect,
outputs: provideLoopOutputsVariablesEffect,
},
// 节点后端数据 -> 前端表单数据
formatOnInit: transformOnInit,
// 前端表单数据 -> 节点后端数据
formatOnSubmit: transformOnSubmit,
};

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 { PrivateScopeProvider } from '@coze-workflow/variable';
import { NodeConfigForm } from '@/node-registries/common/components';
import {
BatchConcurrentSizeField,
BatchInputsField,
BatchOutputsField,
BatchSettingsSection,
BatchSizeField,
} from './fields';
import { BatchPath } from './constants';
export const BatchFormRender = () => (
<NodeConfigForm>
<PrivateScopeProvider>
<BatchSettingsSection>
<BatchConcurrentSizeField name={BatchPath.ConcurrentSize} />
<BatchSizeField name={BatchPath.BatchSize} />
</BatchSettingsSection>
<BatchInputsField name={BatchPath.Inputs} />
</PrivateScopeProvider>
<BatchOutputsField name={BatchPath.Outputs} />
</NodeConfigForm>
);

View File

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

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.
*/
import { PrivateScopeProvider } from '@coze-workflow/variable';
import {
InputParameters,
Outputs,
} from '@/components/node-render/node-render-new/fields';
import { BatchPort } from './batch-content';
export const BatchContent = () => (
<PrivateScopeProvider>
<InputParameters />
<Outputs />
<BatchPort />
</PrivateScopeProvider>
);

View File

@@ -0,0 +1,83 @@
/*
* 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 {
WorkflowNodeEntity,
WorkflowSubCanvas,
} from '@flowgram-adapter/free-layout-editor';
import {
DEFAULT_NODE_META_PATH,
DEFAULT_OUTPUTS_PATH,
} from '@coze-workflow/nodes';
import {
StandardNodeType,
type WorkflowNodeJSON,
type WorkflowNodeRegistry,
} from '@coze-workflow/base';
import { type NodeTestMeta } from '@/test-run-kit';
import { test } from './node-test';
import { BATCH_FORM_META } from './form-meta';
import { BatchPath, BatchSize } from './constants';
import { createBatchFunction, getBatchFunctionID } from './batch-function';
export const BATCH_NODE_REGISTRY: WorkflowNodeRegistry<NodeTestMeta> = {
type: StandardNodeType.Batch,
meta: {
nodeDTOType: StandardNodeType.Batch,
style: {
width: BatchSize.width,
},
size: BatchSize,
nodeMetaPath: DEFAULT_NODE_META_PATH,
outputsPath: DEFAULT_OUTPUTS_PATH,
inputParametersPath: BatchPath.Inputs, // 入参路径,试运行等功能依赖该路径提取参数
useDynamicPort: true,
defaultPorts: [
{ type: 'input' },
{ type: 'output', portID: 'batch-output' },
{ type: 'output', portID: 'batch-output-to-function', disabled: true },
],
subCanvas: (node: WorkflowNodeEntity): WorkflowSubCanvas | undefined => {
const parentNode = node;
const canvasNodeID = getBatchFunctionID(parentNode.id);
const canvasNode = node.document.getNode(canvasNodeID);
if (!canvasNode) {
return undefined;
}
const subCanvas: WorkflowSubCanvas = {
isCanvas: false,
parentNode,
canvasNode,
};
return subCanvas;
},
test,
helpLink: '/open/docs/guides/batch_node',
},
variablesMeta: {
outputsPathList: [],
inputsPathList: [
BatchPath.Inputs,
// 'outputs', // WARNING: 加上 outputs 会导致这一数据清空
],
},
formMeta: BATCH_FORM_META,
onCreate(node, json) {
createBatchFunction(node, json as unknown as WorkflowNodeJSON);
},
};

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import {
generateParametersToProperties,
getRelatedInfo,
generateEnvToRelatedContextProperties,
} from '@/test-run-kit';
import { type NodeTestMeta } from '@/test-run-kit';
export const test: NodeTestMeta = {
async generateRelatedContext(_, context) {
const { isInProject, workflowId, spaceId } = context;
if (isInProject) {
return {};
}
const related = await getRelatedInfo({ workflowId, spaceId });
return generateEnvToRelatedContextProperties(related);
},
generateFormSettingProperties(node) {
const { formModel } = node.getData(FlowNodeFormData);
const data = formModel.getFormItemValueByPath('/inputs');
return generateParametersToProperties(
[
{
name: 'concurrentSize',
title: I18n.t('workflow_maximum_parallel_runs'),
input: data.concurrentSize,
},
{
name: 'batchSize',
title: I18n.t('workflow_maximum_run_count'),
input: data.batchSize,
},
],
{ node },
);
},
generateFormInputProperties(node) {
const formData = node
.getData(FlowNodeFormData)
.formModel.getFormItemValueByPath('/');
const parameters = formData?.inputs?.inputParameters;
return generateParametersToProperties(parameters, { node });
},
};

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 InputValueVO } from '@coze-workflow/base';
export interface FormData {
inputs: { inputParameters: InputValueVO[] };
}

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 { get } from 'lodash-es';
import { type InputValueVO } from '@coze-workflow/base';
import { BatchPath } from '../../constants';
export const getBatchInputNames = ({ value, formValues }): string[] => {
const batchInputs: InputValueVO[] = get(formValues, BatchPath.Inputs) ?? [];
return batchInputs.map(input => input.name).filter(Boolean) as string[];
};

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.
*/
/* eslint-disable @typescript-eslint/naming-convention*/
import { createNodeInputNameValidate } from '@/nodes-v2/components/node-input-name/validate';
import { I18n } from '@coze-arch/i18n';
import { getBatchInputNames } from './get-batch-input-names';
export const BatchInputNameValidator = createNodeInputNameValidate({
getNames: getBatchInputNames,
invalidValues: {
index: I18n.t('workflow_loop_name_no_index_wrong'),
},
});

View File

@@ -0,0 +1,22 @@
/*
* 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/naming-convention*/
import { createValueExpressionInputValidate } from '@/node-registries/common/validators';
export const BatchInputValueValidator = createValueExpressionInputValidate({
required: true,
});

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 { get } from 'lodash-es';
import { type InputValueVO } from '@coze-workflow/base';
import { BatchPath } from '../../constants';
export const getBatchOutputNames = ({ value, formValues }): string[] => {
const batchOutputs: InputValueVO[] = get(formValues, BatchPath.Outputs) ?? [];
return batchOutputs.map(input => input.name).filter(Boolean) as string[];
};

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.
*/
/* eslint-disable @typescript-eslint/naming-convention*/
import { createNodeInputNameValidate } from '@/nodes-v2/components/node-input-name/validate';
import { getBatchOutputNames } from './get-batch-output-names';
export const BatchOutputNameValidator = createNodeInputNameValidate({
getNames: getBatchOutputNames,
});

View File

@@ -0,0 +1,19 @@
/*
* 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 { BatchInputNameValidator } from './batch-input-name';
export { BatchOutputNameValidator } from './batch-output-name';
export { BatchInputValueValidator } from './batch-input-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.
*/
import { type NodeDataDTO } from '@coze-workflow/base';
import { type FormData } from './types';
/**
* 节点后端数据 -> 前端表单数据
*/
export const transformOnInit = (value: NodeDataDTO) => value;
/**
* 前端表单数据 -> 节点后端数据
* @param value
* @returns
*/
export const transformOnSubmit = (value: FormData): NodeDataDTO =>
value as unknown as NodeDataDTO;

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 {
ValidateTrigger,
type FormMetaV2,
} from '@flowgram-adapter/free-layout-editor';
import { nodeMetaValidate } from '@/nodes-v2/materials/node-meta-validate';
import { fireNodeTitleChange } from '@/node-registries/common/effects';
import { type FormData } from './types';
import { FormRender } from './form';
import { transformOnInit, transformOnSubmit } from './data-transformer';
export const BREAK_FORM_META: FormMetaV2<FormData> = {
// 节点表单渲染
render: () => <FormRender />,
// 验证触发时机
validateTrigger: ValidateTrigger.onChange,
// 验证规则
validate: {
nodeMeta: nodeMetaValidate,
},
// 副作用管理
effect: {
nodeMeta: fireNodeTitleChange,
},
// 节点后端数据 -> 前端表单数据
formatOnInit: transformOnInit,
// 前端表单数据 -> 节点后端数据
formatOnSubmit: transformOnSubmit,
};

View File

@@ -0,0 +1,19 @@
/*
* 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 { NodeConfigForm } from '@/node-registries/common/components';
export const FormRender = () => <NodeConfigForm />;

View File

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

View File

@@ -0,0 +1,19 @@
/*
* 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 function BreakContent() {
return <></>;
}

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 { DEFAULT_NODE_META_PATH } from '@coze-workflow/nodes';
import {
StandardNodeType,
type WorkflowNodeRegistry,
} from '@coze-workflow/base';
import { BREAK_FORM_META } from './form-meta';
export const BREAK_NODE_REGISTRY: WorkflowNodeRegistry = {
type: StandardNodeType.Break,
meta: {
isNodeEnd: true,
hideTest: true,
nodeDTOType: StandardNodeType.Break,
defaultPorts: [{ type: 'input' }],
size: { width: 360, height: 67.86 },
nodeMetaPath: DEFAULT_NODE_META_PATH,
},
formMeta: BREAK_FORM_META,
getOutputPoints: () => [], // Break 节点没有输出
};

View File

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

View File

@@ -0,0 +1,67 @@
/*
* 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 { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
import { type InputValueVO, type OutputValueVO } from '@coze-workflow/base';
import { ConfigProvider } from '@coze-arch/coze-design';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import { type CodeEditorValue } from '@/form-extensions/setters/code/types';
import { type InputParams } from '@/form-extensions/setters/code/hooks/use-ide-input-output-type';
import { CodeSetterContext } from '@/form-extensions/setters/code/context';
import { CodeEditorWithBizIDE } from '@/form-extensions/setters/code/code-with-biz-ide';
import { useField, withField } from '@/form';
export const CodeField = withField(
({
tooltip,
outputParams,
inputParams,
}: {
tooltip?: string;
outputParams?: OutputValueVO[];
inputParams?: InputValueVO[];
}) => {
const { value, onChange, errors } = useField<CodeEditorValue>();
const readonly = useReadonly();
const feedbackText = errors?.[0]?.message || '';
const feedbackStatus = feedbackText ? 'error' : undefined;
const flowNodeEntity = useCurrentEntity();
return (
<ConfigProvider getPopupContainer={() => document.body}>
<CodeSetterContext.Provider
value={{
readonly,
flowNodeEntity,
}}
>
<CodeEditorWithBizIDE
feedbackStatus={feedbackStatus}
feedbackText={feedbackText}
inputParams={inputParams as InputParams}
onChange={onChange}
outputParams={outputParams}
outputPath={'/outputs'}
tooltip={tooltip}
value={value}
/>
</CodeSetterContext.Provider>
</ConfigProvider>
);
},
);

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 { CodeField } from './code-field';

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 { nanoid } from 'nanoid';
import { ViewVariableType } from '@coze-workflow/variable';
// 路径
export const INPUT_PATH = 'inputParameters';
export const CODE_PATH = 'codeParams';
export const OUTPUT_PATH = 'outputs';
// 默认值
export const DEFAULT_OUTPUTS = [
{
key: nanoid(),
name: 'key0',
type: ViewVariableType.String,
},
{
key: nanoid(),
name: 'key1',
type: ViewVariableType.ArrayString,
},
{
key: nanoid(),
name: 'key2',
type: ViewVariableType.Object,
children: [
{
key: nanoid(),
name: 'key21',
type: ViewVariableType.String,
},
],
},
];
export const DEFAULT_INPUTS = [{ name: 'input' }];

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 { type NodeContext } from '@flowgram-adapter/free-layout-editor';
import { type NodeDataDTO } from '@coze-workflow/base';
import { getDefaultValue } from '@/form-extensions/setters/code/defaults';
import { type FormData } from './types';
import { DEFAULT_INPUTS, DEFAULT_OUTPUTS } from './constants';
/**
* 节点后端数据 -> 前端表单数据
*/
export const transformOnInit = (
value: NodeDataDTO | undefined,
context: NodeContext,
) => {
const { globalState } = context.playgroundContext;
const { isBindDouyin } = globalState;
const defaultCodeParams = getDefaultValue({ isBindDouyin });
// 初始值设置
const initValue = value || {
inputs: {
inputParameters: DEFAULT_INPUTS,
...defaultCodeParams,
},
outputs: DEFAULT_OUTPUTS,
};
const { inputs = {}, ...others } = initValue;
return {
...others,
inputParameters: inputs.inputParameters,
codeParams: {
code: inputs.code,
language: inputs.language,
},
nodeMeta: value?.nodeMeta,
};
};
/**
* 前端表单数据 -> 节点后端数据
* @param value
* @returns
*/
export const transformOnSubmit = (value: FormData) => ({
nodeMeta: value.nodeMeta,
inputs: {
inputParameters: value.inputParameters,
...value.codeParams,
},
outputs: value.outputs,
});

View File

@@ -0,0 +1,62 @@
/*
* 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 {
ValidateTrigger,
type FormMetaV2,
} from '@flowgram-adapter/free-layout-editor';
import { nodeMetaValidate } from '@/nodes-v2/materials/node-meta-validate';
import {
fireNodeTitleChange,
provideNodeOutputVariablesEffect,
} from '@/node-registries/common/effects';
import { outputTreeMetaValidator } from '../common/fields/outputs';
import { createCodeInputsValidator } from './validators/create-code-inputs-validator';
import { codeEmptyValidator } from './validators/code-empty-validator';
import { type FormData } from './types';
import { FormRender } from './form';
import { transformOnInit, transformOnSubmit } from './data-transformer';
import { CODE_PATH, OUTPUT_PATH } from './constants';
export const CODE_FORM_META: FormMetaV2<FormData> = {
// 节点表单渲染
render: () => <FormRender />,
// 验证触发时机
validateTrigger: ValidateTrigger.onChange,
// 验证规则
validate: {
nodeMeta: nodeMetaValidate,
...createCodeInputsValidator(),
[CODE_PATH]: codeEmptyValidator,
[OUTPUT_PATH]: outputTreeMetaValidator,
},
// 副作用管理
effect: {
nodeMeta: fireNodeTitleChange,
outputs: provideNodeOutputVariablesEffect,
},
// 节点后端数据 -> 前端表单数据
formatOnInit: transformOnInit,
// 前端表单数据 -> 节点后端数据
formatOnSubmit: transformOnSubmit,
};

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { useForm } from '@flowgram-adapter/free-layout-editor';
import { NodeConfigForm } from '@/node-registries/common/components';
import { OutputsField, InputsParametersField } from '../common/fields';
import { CODE_PATH, INPUT_PATH, OUTPUT_PATH } from './constants';
import { CodeField } from './components';
export const FormRender = () => {
const form = useForm();
return (
<NodeConfigForm>
<InputsParametersField
name={INPUT_PATH}
tooltip={I18n.t('workflow_detail_code_input_tooltip')}
isTree={true}
/>
<CodeField
name={CODE_PATH}
tooltip={I18n.t('workflow_detail_code_code_tooltip')}
inputParams={form.getValueIn(INPUT_PATH)}
outputParams={form.getValueIn(OUTPUT_PATH)}
hasFeedback={false}
/>
<OutputsField
title={I18n.t('workflow_detail_node_output')}
tooltip={I18n.t('workflow_detail_code_output_tooltip')}
jsonImport={false}
id="code-node-outputs"
name={OUTPUT_PATH}
hasFeedback={false}
/>
</NodeConfigForm>
);
};

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
DEFAULT_NODE_META_PATH,
DEFAULT_NODE_SIZE,
DEFAULT_OUTPUTS_PATH,
} from '@coze-workflow/nodes';
import {
StandardNodeType,
type WorkflowNodeRegistry,
} from '@coze-workflow/base';
import { type NodeTestMeta } from '@/test-run-kit';
import { test } from './node-test';
import { CODE_FORM_META } from './form-meta';
import { INPUT_PATH } from './constants';
export const CODE_NODE_REGISTRY: WorkflowNodeRegistry<NodeTestMeta> = {
type: StandardNodeType.Code,
meta: {
nodeDTOType: StandardNodeType.Code,
size: DEFAULT_NODE_SIZE,
style: {
width: 484,
},
test,
nodeMetaPath: DEFAULT_NODE_META_PATH,
outputsPath: DEFAULT_OUTPUTS_PATH,
inputParametersPath: INPUT_PATH, // 入参路径,试运行等功能依赖该路径提取参数
enableCopilotGenerateTestNodeForm: true,
helpLink: '/open/docs/guides/code_node',
},
formMeta: CODE_FORM_META,
};

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 { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import { generateParametersToProperties } from '@/test-run-kit';
import { type NodeTestMeta } from '@/test-run-kit';
export const test: NodeTestMeta = {
generateFormInputProperties(node) {
const formData = node
.getData(FlowNodeFormData)
.formModel.getFormItemValueByPath('/');
const parameters = formData?.inputParameters;
return generateParametersToProperties(parameters, { node });
},
};

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 OutputValueVO,
type InputValueVO,
type NodeDataDTO,
} from '@coze-workflow/base';
import { type CodeEditorValue } from '@/form-extensions/setters/code/types';
export interface FormData {
inputParameters: InputValueVO[];
nodeMeta: NodeDataDTO['nodeMeta'];
outputs: OutputValueVO[];
codeParams: CodeEditorValue;
}

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 { I18n } from '@coze-arch/i18n';
import { type Validate } from '@flowgram-adapter/free-layout-editor';
export const codeEmptyValidator: Validate = ({ value }) => {
const code = value?.code;
if (!code) {
return I18n.t('workflow_running_results_error_code');
}
};

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 { type Validate } from '@flowgram-adapter/free-layout-editor';
import { createInputTreeValidator } from '../../common/validators/create-input-tree-validator';
export function createCodeInputsValidator(): { [key: string]: Validate } {
return {
inputParameters: createInputTreeValidator(),
};
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention -- todo */
/** 备注默认尺寸 */
export const CommentDefaultSize = {
width: 240,
height: 150,
};
/** 备注默认值 */
export const CommentDefaultNote = JSON.stringify([
{
type: 'paragraph',
children: [{ text: '' }],
},
]);
export const CommentDefaultSchemaType = 'slate';
export const CommentDefaultVO = {
schemaType: CommentDefaultSchemaType,
note: CommentDefaultNote,
size: CommentDefaultSize,
};
export const CommentDefaultDTO = {
inputs: {
schemaType: CommentDefaultSchemaType,
note: CommentDefaultNote,
},
size: CommentDefaultSize,
};

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
StandardNodeType,
type WorkflowNodeRegistry,
} from '@coze-workflow/base';
import type { ICommentNodeVO, ICommentNodeDTO } from './type';
import {
CommentDefaultDTO,
CommentDefaultNote,
CommentDefaultVO,
CommentDefaultSize,
CommentDefaultSchemaType,
} from './constant';
export const COMMENT_NODE_REGISTRY: WorkflowNodeRegistry = {
type: StandardNodeType.Comment,
meta: {
disableSideSheet: true,
nodeDTOType: StandardNodeType.Comment,
renderKey: StandardNodeType.Comment,
size: {
width: 240,
height: 150,
},
},
formMeta: {
render: () => <></>,
formatOnInit: (
value: ICommentNodeDTO = CommentDefaultDTO,
): ICommentNodeVO => {
const { inputs, ...rest } = value;
return {
...rest,
schemaType: inputs?.schemaType ?? CommentDefaultSchemaType, // 默认使用 slate 格式,后续考虑支持其他格式
note: inputs?.note ?? CommentDefaultNote,
size: value.size ?? CommentDefaultSize,
};
},
formatOnSubmit(value: ICommentNodeVO = CommentDefaultVO): ICommentNodeDTO {
const { note, schemaType, ...rest } = value;
return {
...rest,
inputs: {
schemaType: schemaType ?? CommentDefaultSchemaType, // 默认使用 slate 格式,后续考虑支持其他格式
note: note ?? CommentDefaultNote,
},
size: value.size ?? CommentDefaultSize,
};
},
},
getInputPoints: () => [], // Comment 节点没有输入
getOutputPoints: () => [], // Comment 节点没有输出
};

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.
*/
export interface ICommentNodeVO {
schemaType: string;
note: string;
size: {
width: number;
height: number;
};
}
export interface ICommentNodeDTO {
inputs: {
schemaType: string;
note: string;
};
size: {
width: number;
height: number;
};
}

View File

@@ -0,0 +1,263 @@
/*
* 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 { useCallback, useRef, useState } from 'react';
import classNames from 'classnames';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import {
workflowApi,
CopilotType,
ValueExpression,
ValueExpressionType,
ViewVariableType,
} from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { IconCozStopCircle, IconCozApply } from '@coze-arch/coze-design/icons';
import {
AIButton,
Button,
IconButton,
Input,
Popconfirm,
} from '@coze-arch/coze-design';
import { useRefInput } from '@/hooks/use-ref-input';
import { useGlobalState } from '@/hooks';
import style from './index.module.less';
interface AICronjobSelectProps {
value?: ValueExpression;
onChange: (v: ValueExpression | undefined) => void;
readonly?: boolean;
node: FlowNodeEntity;
needRefInput?: boolean;
hasError?: boolean;
}
const Suffix = ({ onChange }: { onChange: (v: string) => void }) => {
const [visible, setVisible] = useState(false);
const [prompt, setPrompt] = useState('');
const [cronjob, setCronjob] = useState('');
const [aiGenerating, setAIGenerating] = useState(false);
const cancelRequest = useRef<(reason?: unknown) => void>();
const { spaceId, projectId, workflowId } = useGlobalState();
return (
<>
<Popconfirm
className="w-[330px]"
title={I18n.t('workflow_start_trigger_cron_ai', {}, '使用 AI 生成')}
trigger="custom"
stopPropagation
onClickOutSide={() => {
setVisible(false);
}}
content={
<div className="mt-[12px] flex flex-col gap-[12px] text-[12px]">
<div className="flex flex-col gap-[4px] p-[8px] rounded-mini coz-mg-hglt">
{/* <div className="font-medium coz-fg-plus">Sample i18n</div> */}
<div className="coz-fg-primary">
{I18n.t(
'workflow_trigger_cron_gen_sample_placeholder',
{},
'您可以在提示词中用自然语言如“每天18点”将会生成对应的Cron表达式 0 18 * * * 表示每天 18 点执行。',
)}
</div>
</div>
<div className="flex flex-col gap-[4px]">
<div className="flex justify-between">
<div className="text-[14px] font-medium coz-fg-plus">
{I18n.t('Imageflow_prompt', {}, 'Prompt')}
</div>
{!aiGenerating ? (
<AIButton
size="small"
disabled={!prompt}
onClick={async () => {
try {
setAIGenerating(true);
const rs = await workflowApi.CopilotGenerate({
space_id: spaceId,
project_id: projectId ?? '',
copilot_type: CopilotType.CRONTAB,
query: prompt,
workflow_id: workflowId,
});
setCronjob(rs?.data?.content ?? '');
} finally {
setAIGenerating(false);
}
}}
color="aihglt"
>
{I18n.t('workflow_start_trigger_cron_gen', {}, '生成')}
</AIButton>
) : (
<IconButton
size="small"
onClick={() => {
try {
cancelRequest.current?.('cancel');
} finally {
setAIGenerating(false);
}
}}
icon={<IconCozStopCircle />}
color="aihglt"
>
{I18n.t('workflow_start_trigger_cron_gen_stop', {}, '停止')}
</IconButton>
)}
</div>
<Input
size="small"
value={prompt}
placeholder={I18n.t(
'workflow_trigger_cron_gen_prompt_placeholder',
{},
'示例每天18点',
)}
onChange={setPrompt}
></Input>
</div>
<div className="flex flex-col gap-[4px]">
<div className="text-[14px] font-medium coz-fg-plus">
{I18n.t(
'workflow_start_trigger_cron_generated',
{},
'生成的 Cron 表达式',
)}
</div>
<div className="flex flex-row gap-[8px]">
<Input
loading={aiGenerating}
size="small"
disabled
value={cronjob}
onChange={setCronjob}
></Input>
<Button
size="small"
loading={aiGenerating}
disabled={!cronjob}
onClick={() => {
if (cronjob) {
onChange(cronjob);
setVisible(false);
}
}}
>
{I18n.t('workflow_start_trigger_cron_fillin', {}, '填入')}
</Button>
</div>
</div>
</div>
}
visible={visible}
onVisibleChange={v => {
setVisible(v);
}}
onCancel={() => {
setVisible(false);
}}
okText={''}
cancelText={I18n.t('workflow_start_trigger_cron_cancel', {}, '取消')}
>
<div>
<AIButton
size="mini"
onClick={() => setVisible(true)}
color="aihglt"
onlyIcon
/>
</div>
</Popconfirm>
</>
);
};
export const AICronjobSelect = ({
value,
onChange: _onChange,
readonly,
node,
needRefInput = false,
hasError,
}: AICronjobSelectProps) => {
const onLiteralChange = useCallback(
(v: string) => {
_onChange({
type: ValueExpressionType.LITERAL,
content: v,
});
},
[_onChange],
);
const isRef = value?.type && ValueExpression.isRef(value);
const { renderVariableSelect, renderVariableDisplay } = useRefInput({
value,
onChange: _onChange,
readonly,
node,
style: { width: '100%' },
disabledTypes: ViewVariableType.getComplement([ViewVariableType.String]),
});
return (
<div className="w-full flex flex-row gap-[4px]">
{isRef ? (
renderVariableDisplay({ needWrapper: true })
) : (
<Input
error={hasError}
size="small"
disabled={readonly}
className={classNames(['w-full flex-1', style.input])}
value={value?.content as string}
placeholder={I18n.t(
'workflow_start_trigger_cron_ai_sample',
{},
'示例:您可以填写 cron 表达式。例如0 18 * * * 表示每天 18 点执行。',
)}
onChange={onLiteralChange}
suffix={
<div className="flex flex-row gap-[4px]">
<Suffix onChange={onLiteralChange} />
{needRefInput ? (
<div>
{renderVariableSelect(
<IconButton
size="mini"
color="secondary"
icon={<IconCozApply className="text-[16px]" />}
/>,
)}
</div>
) : undefined}
</div>
}
></Input>
)}
</div>
);
};

View File

@@ -0,0 +1,257 @@
/*
* 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 React, { useRef, type ReactNode, useCallback } from 'react';
import { isUndefined } from 'lodash-es';
import CronGenerator from 'cron-string-generator';
import {
type ValueExpression,
ValueExpressionType,
} from '@coze-workflow/base/types';
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { CustomError } from '@coze-arch/bot-error';
import { Cascader, type CascaderData } from '@coze-arch/coze-design';
const hoursSize = 24;
const padStart = 2;
const hoursLeaf: CascaderData[] = Array.from(new Array(hoursSize).keys()).map(
i => ({
label: i.toString().padStart(padStart, '0').concat(':00'),
value: i,
isLeaf: true,
}),
);
const weekDaysNode: () => CascaderData[] = () => {
const size = 7;
return Array.from({ length: size }).map((item, index) => ({
label: I18n.t('bot_task_preset_day_of_week', { day: index }),
value: index,
children: hoursLeaf,
}));
};
const monthDaysNode: () => CascaderData[] = () => {
const size = 31;
return Array.from({ length: size }).map((item, index) => ({
label: I18n.t('bot_task_preset_day_of_month', { day: index + 1 }),
value: index + 1,
children: hoursLeaf,
}));
};
const intervalDaysNode: () => CascaderData[] = () => {
const size = 5;
const offset = 2;
return Array.from({ length: size }).map((item, index) => ({
label: I18n.t('bot_task_preset_day_of_month', { day: index + offset }),
value: index + offset,
children: hoursLeaf,
}));
};
const triggeredEveryday = () => I18n.t('bot_task_preset_triggered_everyday');
const triggeredEveryweek = () => I18n.t('bot_task_preset_triggered_everyweek');
const triggeredMonthly = () => I18n.t('bot_task_preset_triggered_monthly');
const triggeredInterval = () => I18n.t('bot_task_preset_triggered_interval');
const treeData: () => CascaderData[] = () => [
{
label: triggeredEveryday(),
value: 'daily',
children: hoursLeaf,
},
{
label: triggeredEveryweek(),
value: 'weekly',
children: weekDaysNode(),
},
{
label: triggeredMonthly(),
value: 'monthly',
children: monthDaysNode(),
},
{
label: triggeredInterval(),
value: 'intervalDaily',
children: intervalDaysNode(),
},
];
type TaskSchedule = [string, number] | [string, number, number];
const createCron = (schedule: TaskSchedule) => {
const head = schedule[0];
const cronGenerator = new CronGenerator();
if (head === 'daily') {
const hour = schedule[1];
return cronGenerator.every(1).days().atHour(hour).atMinute(0).toString();
}
const day = schedule[1];
const hour = schedule[2];
if (isUndefined(hour)) {
throw new CustomError(
ReportEventNames.parmasValidation,
'invalid schedule',
);
}
if (head === 'weekly') {
return cronGenerator.atHour(hour).atMinute(0).onDaysOfWeek(day).toString();
}
if (head === 'monthly') {
return cronGenerator.atHour(hour).atMinute(0).onDaysOfMonth(day).toString();
}
return cronGenerator.every(day).days().atHour(hour).atMinute(0).toString();
};
export const parseCron: (cron: string) => TaskSchedule = cronExpr => {
// 目前需求只有 5 位 分、时、day of month、月、day of week
// bytescheduler 只支持 6 位 秒、分、时、day of month、月、day of week
const cronUnits = cronExpr?.split?.(' ').slice?.(1);
const hour = cronUnits?.at(1);
const dayOfMonthIndex = 2;
const dayOfMonth = cronUnits?.at(dayOfMonthIndex);
const dayOfWeek = cronExpr?.at(-1);
const numberHour = Number(hour);
// 通配符
const wildcard = '*';
if (dayOfWeek !== wildcard) {
return ['weekly', Number(dayOfWeek), numberHour];
}
if (dayOfMonth !== wildcard) {
if (dayOfMonth?.startsWith(wildcard)) {
return ['intervalDaily', Number(dayOfMonth.split('/').at(1)), numberHour];
}
return ['monthly', Number(dayOfMonth), numberHour];
}
return ['daily', numberHour];
};
// 将key转化为对应文案
// export const getOptionNodeText = (val: TaskSchedule): string[] => {
// let resultData = treeData();
// const nodeList = val?.map(item => {
// const findData = resultData.find(data => data.value === item);
// if (findData) {
// resultData = findData?.children || [];
// return String(findData.label) ?? '';
// } else {
// return '';
// }
// });
// return nodeList;
// };
const renderDisplay = (nodeList: string[]) => {
const head = nodeList.at(0);
const formatHours = (hours?: string) => {
const formattedHours = hours?.startsWith?.('0') ? hours.slice?.(1) : hours;
return formattedHours;
};
if (head === triggeredEveryday()) {
const hours = nodeList.at(1);
return I18n.t('bot_task_preset_everyday_task', {
time: formatHours(hours),
});
}
const numberRegx = /[0-9]+/;
const dayNode = nodeList.at(1);
const days = Number(dayNode?.match?.(numberRegx)?.[0]);
const hoursIndex = 2;
const hours = nodeList.at(hoursIndex);
const formattedHours = formatHours(hours);
if (head === triggeredEveryweek()) {
return I18n.t('bot_task_preset_everyweek_task', {
day: dayNode,
time: formattedHours,
});
}
if (head === triggeredMonthly()) {
return I18n.t('bot_task_preset_monthly_task', {
day: days,
time: formattedHours,
});
}
if (head === triggeredInterval()) {
return I18n.t('bot_task_preset_interval_task', {
day: days,
time: formattedHours,
});
}
return '';
};
interface FixCronjobSelectProps {
value?: ValueExpression;
onChange?: (e: ValueExpression) => void;
readonly?: boolean;
hasError?: boolean;
}
/** 选择时间和时区组件 */
export const FixCronjobSelect: React.FC<FixCronjobSelectProps> = ({
value: _value,
onChange: _onChange,
readonly,
hasError,
}) => {
const displayText = useRef<ReactNode>('');
const value = _value?.content as string;
const onChange = useCallback(
(v: string) => {
_onChange?.({
type: ValueExpressionType.LITERAL,
content: v,
});
},
[_onChange],
);
return (
<>
<Cascader
hasError={hasError}
size="small"
className="w-full"
disabled={readonly}
showClear={false}
placeholder={I18n.t('task_preset_trigger_time')}
value={value ? parseCron(value) : void 0}
onChange={v => {
if (Array.isArray(v) && v.length) {
// 生成的 cron 表达式是 5 位 但需要 6 位
const cronExpr = `0 ${createCron(v as TaskSchedule)}`;
onChange?.(cronExpr);
return;
}
onChange?.('');
}}
treeData={treeData()}
displayRender={nodes => {
if (Array.isArray(nodes)) {
displayText.current = renderDisplay(nodes);
return displayText.current;
}
}}
/>
</>
);
};

View File

@@ -0,0 +1,7 @@
.input{
:global{
.semi-input-suffix{
margin-right: 0;
}
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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 { useMemo, type FC } from 'react';
import { useWatchFormErrors } from '@flowgram-adapter/free-layout-editor';
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
import { cronJobTranslator } from '@coze-workflow/components';
import { CronJobType, type CronJobValue } from '@coze-workflow/nodes';
import { ValueExpressionType, ValueExpression } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { Select } from '@coze-arch/coze-design';
import { Text } from '@/form-extensions/components/text';
import { type DynamicComponentProps } from '../dynamic-form';
import { FixCronjobSelect } from './fix-cronjob';
import { AICronjobSelect } from './ai-cronjob';
type CronJobSelectProps = DynamicComponentProps<CronJobValue> & {
needRefInput?: boolean;
className?: string;
};
export const CronJobSelect: FC<CronJobSelectProps> = ({
needRefInput,
className = '',
value = {
type: CronJobType.Selecting,
content: {
type: ValueExpressionType.LITERAL,
content: '',
},
},
onChange: _onChange,
readonly,
}) => {
const node = useCurrentEntity();
const errors = useWatchFormErrors(node);
const hasError = (errors ?? []).length > 0;
const type = value.type ?? CronJobType.Selecting;
const onChange = (content: ValueExpression | undefined) => {
_onChange({
type,
content: content ?? {
type: ValueExpressionType.LITERAL,
},
});
};
const cronjobTranslateText = useMemo(() => {
if (
hasError ||
type === CronJobType.Selecting ||
(value.content && ValueExpression.isRef(value.content))
) {
return '';
}
return cronJobTranslator(value.content?.content as unknown as string);
}, [hasError, value.content, type]);
return (
<div className={className}>
<div className="flex flex-row gap-[2px]">
<Select
size="small"
value={type}
className="w-fit mb-[4px]"
disabled={readonly}
onChange={v => {
_onChange({
type: v as CronJobType,
content: {
type: ValueExpressionType.LITERAL,
},
});
}}
optionList={[
{
label: I18n.t(
'workflow_start_trigger_cron_option',
{},
'选择预设时间',
),
value: CronJobType.Selecting,
},
{
label: I18n.t(
'workflow_start_trigger_cron_job',
{},
'使用Cron表达式',
),
value: CronJobType.Cronjob,
},
]}
></Select>
<div className="flex-1 overflow-hidden">
{type === CronJobType.Selecting ? (
<FixCronjobSelect
hasError={hasError}
value={value.content}
onChange={onChange}
readonly={readonly}
/>
) : (
<AICronjobSelect
hasError={hasError}
value={value.content}
onChange={onChange}
readonly={readonly}
node={node}
needRefInput={needRefInput}
/>
)}
</div>
</div>
{cronjobTranslateText ? (
<div className="coz-mg-primary w-full h-[24px] flex flex-row items-center justify-center rounded-[4px]">
<Text text={cronjobTranslateText} className="text-[12px]" />
</div>
) : null}
</div>
);
};

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 { ViewVariableType } from '@coze-workflow/base';
import { Tag } from '@coze-arch/coze-design';
const ViewDataTypeMap = {
[ViewVariableType.String]: 'String',
[ViewVariableType.Integer]: 'Integer',
[ViewVariableType.Boolean]: 'Boolean',
[ViewVariableType.Number]: 'Number',
[ViewVariableType.Time]: 'Time',
[ViewVariableType.File]: 'File',
[ViewVariableType.Image]: 'File/Image',
[ViewVariableType.Doc]: 'File/Doc',
[ViewVariableType.Excel]: 'File/Excel',
[ViewVariableType.Code]: 'File/Code',
[ViewVariableType.Ppt]: 'File/PPT',
[ViewVariableType.Txt]: 'File/Text',
[ViewVariableType.Audio]: 'File/Audio',
[ViewVariableType.Zip]: 'File/ZIP',
[ViewVariableType.Video]: 'File/Video',
[ViewVariableType.Svg]: 'File/SVG',
[ViewVariableType.Voice]: 'File/Voice',
};
interface DataTypeTagProps {
type?: ViewVariableType;
disabled?: boolean;
}
export function DataTypeTag({ type, disabled }: DataTypeTagProps) {
return (
<Tag color="primary" size="mini" disabled={disabled}>
{type === undefined ? 'undefined' : ViewDataTypeMap[type]}
</Tag>
);
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo, type FC } from 'react';
import { Field } from '@/form';
import { type DynamicComponentProps, type FormMeta } from './types';
export interface DynamicFormProps {
formMeta: FormMeta;
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
components: Record<string, FC<DynamicComponentProps<any>>>;
// 禁用做触发
onChange?: () => void;
}
export const DynamicForm: FC<DynamicFormProps> = ({
formMeta,
name,
components,
onChange,
}) => {
const fields = useMemo(
() =>
formMeta.map(field => {
const Component =
components[field.setter] ??
(() => <div>component {field.setter} not exist</div>);
return (
<Field
{...field}
key={field.name}
name={`${name}.${field.name}`}
layout={field.layout ?? 'vertical'}
>
{({ value, onChange: _onChange, readonly }) => (
<Component
{...field.setterProps}
value={value}
onChange={(...args) => {
_onChange(...args);
onChange?.();
}}
readonly={readonly}
/>
)}
</Field>
);
}),
[formMeta, name, components],
);
return <>{fields}</>;
};

View File

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

View File

@@ -0,0 +1,36 @@
/*
* 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 interface FormItemMeta {
name: string;
label: string;
required?: boolean;
setter: string;
setterProps?: {
defaultValue?: unknown;
[k: string]: unknown;
};
layout?: 'horizontal' | 'vertical';
}
export type FormMeta = FormItemMeta[];
export interface DynamicComponentProps<T> {
value?: T; // 字段值
readonly?: boolean; // 是否只读
disabled?: boolean; // 是否禁用
onChange: (newValue?: T) => void;
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ExpressionEditor,
type ExpressionEditorProps,
} from '@/nodes-v2/components/expression-editor';
import { withField, useField } from '@/form';
export const ExpressionEditorField = withField<
Omit<ExpressionEditorProps, 'value' | 'onChange'> & {
testIDSuffix?: string;
}
>(props => {
const { name, value, onChange, errors, onFocus, onBlur } = useField<string>();
const {
placeholder,
minRows,
maxLength,
disableSuggestion,
disableCounter,
// 旧版逻辑是用name作为testID后缀 新版节点name路径可能会变(如xxx -> inputs.xxx) 需要从外部指定
testIDSuffix = name,
} = props;
return (
<ExpressionEditor
name={testIDSuffix}
value={value}
onChange={newValue => onChange(newValue as string)}
key={name}
placeholder={placeholder}
minRows={minRows}
maxLength={maxLength}
isError={errors && errors?.length > 0}
disableSuggestion={disableSuggestion}
disableCounter={disableCounter}
onFocus={onFocus}
onBlur={onBlur}
/>
);
});

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMemo } from 'react';
import {
Field,
type FieldRenderProps,
useCurrentEntity,
} from '@flowgram-adapter/free-layout-editor';
import { WorkflowNode } from '@coze-workflow/base';
import { useDefaultNodeMeta } from '@/nodes-v2/hooks/use-default-node-meta';
import { type NodeHeaderValue } from '@/nodes-v2/components/node-header';
import { NodeHeader } from '@/nodes-v2';
import { useGlobalState } from '@/hooks';
import { useWatch } from '@/form';
export function Header({
extraOperation,
nodeDisabled,
readonlyAllowDeleteOperation,
}: {
extraOperation?: React.ReactNode;
nodeDisabled?: boolean;
readonlyAllowDeleteOperation?: boolean;
}) {
const defaultNodeMeta = useDefaultNodeMeta();
const node = useCurrentEntity();
const { projectId } = useGlobalState();
const wrappedNode = useMemo(() => new WorkflowNode(node), [node]);
const triggerIsOpen = useWatch<Boolean>('trigger.isOpen');
const showTrigger = useMemo(
() => wrappedNode.registry?.meta?.showTrigger?.({ projectId }),
[projectId],
);
return (
<Field
name={'nodeMeta'}
deps={['outputs', 'batchMode']}
defaultValue={defaultNodeMeta as unknown as NodeHeaderValue}
>
{({ field, fieldState }: FieldRenderProps<NodeHeaderValue>) => (
<NodeHeader
{...field}
showErrorIgnore
errors={fieldState?.errors || []}
hideTest={wrappedNode.registry?.meta?.hideTest}
readonly={wrappedNode.registry?.meta?.headerReadonly}
showTrigger={showTrigger}
triggerIsOpen={triggerIsOpen}
extraOperation={extraOperation}
nodeDisabled={nodeDisabled}
readonlyAllowDeleteOperation={readonlyAllowDeleteOperation}
/>
)}
</Field>
);
}

View File

@@ -0,0 +1,43 @@
/*
* 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 ComponentProps } from 'react';
import { EnumImageModel } from '@coze-workflow/setters';
import { withField, useField } from '@/form';
type ImageModelSelectProps = Omit<
ComponentProps<typeof EnumImageModel>,
'value' | 'onChange'
>;
/**
* 图像模型选择器
*/
export const ImageModelSelectField = withField<ImageModelSelectProps>(props => {
const { value, readonly, onChange, errors } = useField<number>();
return (
<EnumImageModel
value={value}
readonly={readonly}
onChange={newValue => onChange?.(newValue as number)}
validateStatus={errors && errors.length > 0 ? 'error' : undefined}
{...props}
/>
);
});

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { NodeConfigForm } from './node-config-form';
export { DataTypeTag } from './data-type-tag';
export { InputParameters } from './input-parameters';
export { Outputs } from './outputs';
export { ImageModelSelectField } from './image-model-select-field';
export { ExpressionEditorField } from './expression-editor-field';
export { Notify } from './notify';
export { Timezone } from './timezone';
export { CronJobSelect } from './cronjob-select';
export { DynamicForm } from './dynamic-form';

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// TODO 源码待迁移。 开发 cli ,依赖引用路径
export { InputParameters } from '@/components/node-render/node-render-new/fields/input-parameters';

View File

@@ -0,0 +1,67 @@
/*
* 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 PropsWithChildren } from 'react';
import { PublicScopeProvider } from '@coze-workflow/variable';
import { useIsSettingOnError } from '@coze-workflow/nodes';
import { useGlobalState } from '@/hooks';
import { Form } from '@/form';
import { SettingOnError } from '../fields';
import { Header } from './header';
type NodeConfigFormProps = PropsWithChildren<{
extraOperation?: React.ReactNode;
batchModePath?: string;
nodeDisabled?: boolean;
readonlyAllowDeleteOperation?: boolean;
}>;
/**
* NodeConfigForm组件
* 用于展示节点配置表单
* @param children - 子组件,用于渲染表单内容
*/
export function NodeConfigForm({
children,
extraOperation,
batchModePath,
nodeDisabled,
readonlyAllowDeleteOperation,
}: NodeConfigFormProps) {
const { readonly } = useGlobalState();
const isSettingOnError = useIsSettingOnError();
return (
<>
<Header
extraOperation={extraOperation}
nodeDisabled={nodeDisabled}
readonlyAllowDeleteOperation={readonlyAllowDeleteOperation}
/>
<PublicScopeProvider>
<Form readonly={readonly}>
{children}
{isSettingOnError ? (
<SettingOnError batchModePath={batchModePath} />
) : null}
</Form>
</PublicScopeProvider>
</>
);
}

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.
*/
import { type PropsWithChildren } from 'react';
import { PublicScopeProvider } from '@coze-workflow/variable';
import { Header } from './header';
export function Node({ children }: PropsWithChildren) {
return (
<>
<Header />
<PublicScopeProvider>
<>{children}</>
</PublicScopeProvider>
</>
);
}

View File

@@ -0,0 +1,43 @@
/*
* 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 FC } from 'react';
import classnames from 'classnames';
import { Text } from '@/form-extensions/components/text';
export const Notify: FC<{
text: string;
align?: 'left' | 'center' | 'right';
className?: string;
isBreakLine?: boolean;
}> = ({ text, align = 'center', className = '', isBreakLine = false }) => (
<div
className={classnames(
'w-full !px-[8px] !py-[6px] flex flex-row items-center coz-mg-hglt-secondary text-[14px]',
{
'justify-center': align === 'center',
'justify-end': align === 'right',
'justify-start': align === 'left',
},
className,
)}
>
{isBreakLine ? text : <Text text={text} />}
</div>
);

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// TODO 源码待迁移。 开发 cli ,依赖引用路径
export { Outputs } from '@/components/node-render/node-render-new/fields/outputs';

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 默认时区,目前用于 task 模块
*
* 国内UTC+8
* 海外UTC+0
*/
export const DEFAULT_TIME_ZONE = IS_OVERSEA ? 'Etc/GMT+0' : 'Asia/Shanghai';
export const DEFAULT_TIME_ZONE_OFFSET = IS_OVERSEA ? 'UTC+00:00' : 'UTC+08:00';
// 未知时区,用于兼容
export const UNKNOWN_TIME_ZONE_OFFSET = 'Others';

View File

@@ -0,0 +1,15 @@
.dropdown {
:global {
.semi-cascader-option-lists {
height: 260px;
&.semi-cascader-option-lists-empty {
height: auto;
}
}
.semi-cascader-option-empty {
cursor: default;
}
}
}

View File

@@ -0,0 +1,193 @@
/*
* 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 { useMemo, type FC } from 'react';
import { cloneDeep, find, noop } from 'lodash-es';
import classNames from 'classnames';
import { type LiteralExpression } from '@coze-workflow/base';
import { ValueExpressionType } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import {
Cascader,
Highlight,
type CascaderData,
type FilterRenderProps,
} from '@coze-arch/coze-design';
import { type DynamicComponentProps } from '../dynamic-form';
import { generatedTimezones } from './utils/timezone';
import { UNKNOWN_TIME_ZONE_OFFSET } from './const';
import styles from './index.module.less';
type TimezoneProps = DynamicComponentProps<string> & {
className?: string;
showClear?: boolean; // 是否可清空
defaultValue?: string; // 默认值
};
export const Timezone: FC<TimezoneProps> = ({
className,
showClear = false,
defaultValue,
value: _value,
onChange = noop,
readonly,
}) => {
const value = ((_value as unknown as LiteralExpression)?.content ??
_value ??
defaultValue) as string;
// 时区列表
const { timezoneOptions: TIME_ZONE_OPTIONS, timezoneMap: TIME_ZOME_MAP } =
useMemo(() => generatedTimezones(), []);
// 时区选择器选项生成逻辑
const [timezoneOptions, timezoneMap] = useMemo(() => {
const timezoneOptionsBase = cloneDeep(TIME_ZONE_OPTIONS);
const timezoneMapBase = cloneDeep(TIME_ZOME_MAP);
// 用户已选择时区,但当前环境不支持兼容对应时区时,在选项末尾插入未知时区选项兼容
if (value && timezoneMapBase.every(e => e.value !== value)) {
timezoneOptionsBase.push({
value: UNKNOWN_TIME_ZONE_OFFSET,
label: UNKNOWN_TIME_ZONE_OFFSET,
children: [{ value, label: value }],
});
timezoneMapBase.push({
value,
offset: UNKNOWN_TIME_ZONE_OFFSET,
});
}
return [timezoneOptionsBase, timezoneMapBase];
}, []);
const timezoneValue = useMemo(() => {
const item = find(timezoneMap, ['value', value]);
if (item) {
return [item.offset, item.value];
} else {
return undefined;
}
}, [value, timezoneMap]);
// 遍历查询时区偏移量
const findTimezoneValue = (nodes: string[], key: string) => {
if (Array.isArray(nodes) && nodes.length > 1) {
const [offsetValue, timezoneLabel] = nodes;
const item = find(timezoneOptions, ['value', offsetValue]);
if (item && Array.isArray(item.children)) {
const option = find(item.children, [key, timezoneLabel]);
if (option && option.value) {
return option.value as string;
}
}
}
};
const filterSorter = (
first: CascaderData,
second: CascaderData,
inputValue: string,
) => {
if (
Array.isArray(first) &&
first.length > 1 &&
Array.isArray(second) &&
second.length > 1
) {
const keyword: string = (inputValue || '').toLowerCase();
const firstLabelLowerCase: string = (first[1].label || '').toLowerCase();
const secondLabelLowerCase: string = (
second[1].label || ''
).toLowerCase();
const firstLabelArray = firstLabelLowerCase.split(' ');
const secondLabelArray = secondLabelLowerCase.split(' ');
if (firstLabelArray.findIndex(e => e === keyword) === 0) {
return -1;
}
if (secondLabelArray.findIndex(e => e === keyword) === 0) {
return 1;
}
if (firstLabelLowerCase.includes(keyword)) {
return -1;
}
if (secondLabelLowerCase.includes(keyword)) {
return 1;
}
}
return 0;
};
// 自定义高亮逻辑
const filterRender = (filterRenderProps: FilterRenderProps) => {
const { className: cls, inputValue, data, onClick } = filterRenderProps;
// 把多级选项的文案拼起来
const labelString = data.map(e => e.label).join(' / ');
return (
<li
role="menuitem"
className={classNames('semi-cascader-option-flatten', cls)}
onClick={onClick}
>
<span className="semi-cascader-option-label">
<span
aria-hidden="true"
className="semi-cascader-option-icon semi-cascader-option-icon-empty"
></span>
<Highlight
sourceString={labelString}
searchWords={[inputValue]}
highlightStyle={{
color: 'var(--light-usage-primary-color-primary, #4d53e8)',
backgroundColor: 'transparent',
}}
/>
</span>
</li>
);
};
return (
<Cascader
size="small"
className={`${className} w-full`}
dropdownClassName={styles.dropdown}
showClear={showClear}
filterTreeNode
filterSorter={filterSorter}
filterRender={filterRender}
disabled={readonly}
placeholder={I18n.t('task_preset_timezone')}
treeData={timezoneOptions}
value={timezoneValue}
onChange={nodes => {
const newTimezone = findTimezoneValue(nodes as string[], 'value');
onChange({
type: ValueExpressionType.LITERAL,
content: newTimezone,
});
if (newTimezone) {
sendTeaEvent(EVENT_NAMES.select_scheduled_tasks_timezone, {
timezone: newTimezone,
});
}
}}
displayRender={nodes => findTimezoneValue(nodes as string[], 'label')}
/>
);
};

View File

@@ -0,0 +1,109 @@
/*
* 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 { groupBy, toPairs, find, orderBy } from 'lodash-es';
import dayjsUTC from 'dayjs/plugin/utc';
import dayjsTimezone from 'dayjs/plugin/timezone';
import quartersOfYear from 'dayjs/plugin/quarterOfYear';
import isoWeek from 'dayjs/plugin/isoWeek';
import dayjs from 'dayjs';
import { logger } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { type CascaderData } from '@coze-arch/bot-semi/Cascader';
import { DEFAULT_TIME_ZONE, DEFAULT_TIME_ZONE_OFFSET } from '../const';
// dependent on utc plugin
dayjs.extend(isoWeek); // 注意:这里插件的注册顺序不能随意改变,此外重复注册插件可能会有 bug。
dayjs.extend(quartersOfYear);
dayjs.extend(dayjsUTC);
dayjs.extend(dayjsTimezone);
export const dayjsForTimezone = dayjs;
export interface ITimezoneItem {
value: string;
label?: string;
offset: string;
utcOffset?: number;
}
// 获取时区列表
export const generatedTimezones = () => {
let timezoneOptions: CascaderData[] = [];
let timezoneMap: ITimezoneItem[] = [];
try {
// 当前国际化环境
const locale = I18n.language ?? 'en-US';
/**
* 所有时区列表
* (Intl as any) 项目的typescript版本不含有supportedValuesOf方法编译会报错
*/
const options = (Intl as any)
.supportedValuesOf('timeZone')
.map((item: any) => {
const formatDateParts = new Intl.DateTimeFormat(locale, {
timeZone: item,
timeZoneName: 'longGeneric',
}).formatToParts(new Date());
const { value: timeZoneName } = find(formatDateParts, [
'type',
'timeZoneName',
]) as Intl.DateTimeFormatPart;
const targetTime = dayjs().tz(item);
return {
value: item,
label: `${timeZoneName} - ${item}`,
offset: `UTC${targetTime.format('Z')}`,
utcOffset: targetTime.utcOffset(),
};
});
timezoneOptions = orderBy(
toPairs(groupBy(options, 'offset')),
item => {
const [offset] = item;
return find(options, ['offset', offset])?.utcOffset;
},
['asc'],
).map(item => {
const [itemKey, itemValue] = item;
return {
value: itemKey,
label: itemKey,
children: orderBy(itemValue, ['label'], ['asc']),
};
});
timezoneMap = options;
} catch (error: any) {
timezoneOptions = [
{
value: DEFAULT_TIME_ZONE_OFFSET,
label: DEFAULT_TIME_ZONE_OFFSET,
children: [{ value: DEFAULT_TIME_ZONE, label: DEFAULT_TIME_ZONE }],
},
];
timezoneMap = [
{ value: DEFAULT_TIME_ZONE, offset: DEFAULT_TIME_ZONE_OFFSET },
];
// 如果出现无法获取时区信息的情况上报异常到 slardar
logger.persist.error({
message: 'Custom Error: Unable to obtain accurate time zone list',
error,
});
}
return { timezoneOptions, timezoneMap };
};

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.
*/
export { fireNodeTitleChange } from '@/nodes-v2/materials/fire-node-title-change';
export { provideNodeOutputVariablesEffect } from '@/nodes-v2/materials/provide-node-output-variables';
export { provideLoopInputsVariablesEffect } from './provide-loop-inputs-variables';
export { provideLoopOutputsVariablesEffect } from './provide-loop-outputs-variables';

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 { createEffectFromVariableProvider } from '@coze-workflow/variable/src/utils/variable-provider';
import { provideLoopInputsVariables } from '@coze-workflow/variable/src/form-extensions/variable-providers/provide-loop-input-variables';
export const provideLoopInputsVariablesEffect =
createEffectFromVariableProvider(provideLoopInputsVariables);

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 { createEffectFromVariableProvider } from '@coze-workflow/variable/src/utils/variable-provider';
import { provideLoopOutputsVariables } from '@coze-workflow/variable/src/form-extensions/variable-providers/provide-loop-output-variables';
export const provideLoopOutputsVariablesEffect =
createEffectFromVariableProvider(provideLoopOutputsVariables);

View File

@@ -0,0 +1,76 @@
/*
* 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 FC } from 'react';
import { type InputValueVO, useNodeTestId } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { Section } from '@/form';
import { SwitchField } from '../switch-field';
import { ExpressionEditorField } from '../expression-editor-field';
export interface AnswerContentFieldProps {
enableStreamingOutput?: boolean;
editorFieldName: string;
switchFieldName?: string;
title?: string;
tooltip?: string;
switchLabel?: string;
switchTooltip?: string;
switchTestId?: string;
inputParameters?: InputValueVO[];
testId?: string;
}
export const AnswerContentField: FC<AnswerContentFieldProps> = ({
title,
tooltip,
switchLabel,
switchTooltip,
enableStreamingOutput,
editorFieldName,
switchFieldName,
switchTestId,
inputParameters,
testId,
}) => {
const { concatTestId, getNodeSetterId } = useNodeTestId();
return (
<Section
title={title}
tooltip={tooltip}
actions={[
enableStreamingOutput && switchFieldName ? (
<SwitchField
name={switchFieldName}
customLabel={switchLabel}
customTooltip={switchTooltip}
testId={concatTestId(testId ?? '', switchTestId ?? '')}
/>
) : null,
]}
testId={getNodeSetterId(testId ?? '')}
>
<ExpressionEditorField
name={editorFieldName}
placeholder={I18n.t('workflow_detail_end_answer_example')}
inputParameters={inputParameters}
testId={testId}
/>
</Section>
);
};

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 React from 'react';
import { BatchMode } from '@/nodes-v2/components/batch-mode';
import { useField, withField } from '@/form';
export const BatchModeField = withField<{}, string>(() => {
const { name: fieldName, value, onChange, onBlur } = useField<string>();
return (
<BatchMode
name={fieldName}
value={value}
onChange={e => {
onChange?.((e as React.ChangeEvent<HTMLInputElement>).target.value);
onBlur?.();
}}
onBlur={onBlur}
/>
);
});

View File

@@ -0,0 +1,40 @@
/*
* 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 React from 'react';
import { Checkbox } from '@/form-extensions/components/checkbox';
import { useField, withField } from '@/form';
export const CheckboxWithTipsField = withField(
({ text, itemTooltip }: { text: string; itemTooltip?: string }) => {
const { name, value, onChange, readonly } = useField<boolean>();
const context = { meta: { name } };
const options = {
text,
itemTooltip,
};
return (
<Checkbox
options={options}
context={context}
value={!!value}
onChange={(v: boolean) => onChange(v)}
readonly={!!readonly}
/>
);
},
);

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
ExpressionEditor as ExpressionEditorLeagcy,
type ExpressionEditorProps,
} from '@/nodes-v2/components/expression-editor';
import { useField, withField } from '@/form';
type ExpressionEditorFieldProps = Omit<
ExpressionEditorProps,
'value' | 'onChange' | 'onBlur' | 'onFocus'
> & {
dataTestName?: string;
};
function ExpressionEditor(props: ExpressionEditorFieldProps) {
const { name, value, onChange, errors, onBlur, readonly } =
useField<string>();
return (
<ExpressionEditorLeagcy
{...props}
name={props?.dataTestName ?? name}
value={value as string}
onChange={e => onChange(e as string)}
isError={errors && errors?.length > 0}
onBlur={onBlur}
disableSuggestion={readonly}
/>
);
}
export const ExpressionEditorField = withField(ExpressionEditor);

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.
*/
export { InputsParametersField } from './inputs-parameters-field';
export { createInputsValidator } from './inputs-parameters-field/create-inputs-validator';
export { InputsField } from './inputs-parameters-field/inputs-field';
export { OutputsField } from './outputs';
export { ValueExpressionInputField } from './value-expression-input';
export { SettingOnError } from './setting-on-error';
export { ParametersInputGroup } from './parameters-input-group';
export { ParametersInputGroupField } from './parameters-input-group/parameters-input-group-field';
export { ModelSelectField } from './model-select-field';
export { RadioSetterField } from './radio-setter-field';
export { ExpressionEditorField } from './expression-editor-field';
export { CheckboxWithTipsField } from './checkbox-with-tips-field';
export { OutputsDisplayField } from './outputs-display-field';
export { AnswerContentField } from './answer-content-field';
export { SwitchField } from './switch-field';
export { LoopOutputsField } from './loop-outputs-field';
export { BatchModeField } from './batch-mode-field';
export { InputsKVField } from './inputs-kv-field';

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 { I18n } from '@coze-arch/i18n';
export const COLUMNS = [
{
label: I18n.t('workflow_detail_node_parameter_name'),
style: { width: 148 },
},
{ label: I18n.t('workflow_detail_end_output_value') },
];

View File

@@ -0,0 +1,146 @@
/*
* 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 React, { type PropsWithChildren } from 'react';
import { generateInputJsonSchema } from '@coze-workflow/variable';
import {
getInputType,
getSortedInputParameters,
type ApiNodeDetailDTO,
} from '@coze-workflow/nodes';
import {
useNodeTestId,
type InputValueVO,
type VariableMetaDTO,
type DTODefine,
} from '@coze-workflow/base';
import { reporter } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { ValueExpressionInputField } from '@/node-registries/common/fields';
import {
Section,
useField,
ColumnTitles,
withField,
SelectField,
SliderField,
FieldLayout,
} from '@/form';
import { getCustomSetterProps } from '../../utils';
import { COLUMNS } from './constants';
interface InputsProps {
inputsDef: ApiNodeDetailDTO['inputs'];
}
const CUSTOM_SETTER_MAP = {
Select: SelectField,
Slider: SliderField,
};
const CUSTOM_SETTER_STYLE = {
Select: {
width: '100%',
},
};
const renderCustomSetter = (
fieldDef: ApiNodeDetailDTO['inputs'][number],
customSetterProps: Record<string, unknown> & { key: string },
fieldName: string,
) => {
const { title, name, required, description, label } = fieldDef;
const { key, ...setterProps } = customSetterProps;
const CustomSetter = CUSTOM_SETTER_MAP[key];
if (!CustomSetter) {
return null;
}
return (
<FieldLayout
label={title || label || name}
tooltip={description}
required={required}
>
<CustomSetter
{...setterProps}
name={`${fieldName}.${name}`}
style={CUSTOM_SETTER_STYLE[key]}
/>
</FieldLayout>
);
};
export const InputsKVField = withField(
({ inputsDef }: InputsProps & PropsWithChildren) => {
const { name: fieldName } = useField<InputValueVO>();
const { getNodeSetterId } = useNodeTestId();
return (
<Section
title={I18n.t('workflow_detail_node_input')}
tooltip={I18n.t('workflow_detail_api_input_tooltip')}
testId={getNodeSetterId(fieldName)}
actions={[]}
>
<ColumnTitles columns={COLUMNS} />
<div className="flex flex-col gap-[8px]">
{getSortedInputParameters(inputsDef)?.map(fieldDef => {
const { title, name, required, description, label } = fieldDef;
const { inputType, disabledTypes } = getInputType(
fieldDef as DTODefine.InputVariableDTO,
);
let jsonSchema: ReturnType<typeof generateInputJsonSchema>;
try {
jsonSchema = generateInputJsonSchema(fieldDef as VariableMetaDTO);
} catch (error) {
jsonSchema = undefined;
reporter.error({
message: 'workflow_plugin_generate_input_json_schema_error',
error,
});
}
const customSetterProps = getCustomSetterProps(fieldDef);
if (customSetterProps?.key) {
return renderCustomSetter(fieldDef, customSetterProps, fieldName);
}
return (
<ValueExpressionInputField
key={`${fieldName}.${name}`}
label={title || label || name}
tooltip={description}
required={required}
inputType={inputType}
disabledTypes={disabledTypes}
name={`${fieldName}.${name}`}
literalConfig={jsonSchema ? { jsonSchema } : undefined}
/>
);
})}
</div>
</Section>
);
},
);

View File

@@ -0,0 +1,45 @@
/*
* 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 Validate } from '@flowgram-adapter/free-layout-editor';
import { createValueExpressionInputValidate } from '@/nodes-v2/materials/create-value-expression-input-validate';
import { createNodeInputNameValidate } from '@/nodes-v2/components/node-input-name/validate';
import { createInputTreeValidator } from '../../validators/create-input-tree-validator';
export function createInputsValidator(isTree: boolean): {
[key: string]: Validate;
} {
if (isTree) {
return {
'inputs.inputParameters': createInputTreeValidator(),
};
}
return {
'inputs.inputParameters.*.name': createNodeInputNameValidate({
getNames: ({ formValues }) =>
(get(formValues, 'inputs.inputParameters') || []).map(
item => item.name,
),
}),
'inputs.inputParameters.*.input': createValueExpressionInputValidate({
required: true,
}),
};
}

View File

@@ -0,0 +1,82 @@
/*
* 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 ViewVariableType, type InputValueVO } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { type FieldProps } from '@/form';
import { type NodeInputNameProps } from './node-input-name/type';
import { InputsTreeField } from './inputs-tree-field';
import { InputsField } from './inputs-field';
interface InputsSectionProps extends FieldProps<InputValueVO[]> {
title?: string;
tooltip?: React.ReactNode;
isTree?: boolean;
paramsTitle?: string;
expressionTitle?: string;
testId?: string;
disabledTypes?: ViewVariableType[];
onAppend?: () => InputValueVO;
inputPlaceholder?: string;
literalDisabled?: boolean;
nameProps?: Partial<NodeInputNameProps>;
customReadonly?: boolean;
}
export const InputsParametersField = ({
name = 'inputs.inputParameters',
title = I18n.t('workflow_detail_node_parameter_input'),
tooltip = I18n.t('workflow_240218_07'),
paramsTitle,
expressionTitle,
disabledTypes,
defaultValue,
onAppend,
inputPlaceholder,
literalDisabled,
isTree,
nameProps,
customReadonly,
testId,
}: InputsSectionProps) =>
isTree ? (
<InputsTreeField
name={name}
defaultValue={defaultValue}
title={title}
tooltip={tooltip}
customReadonly={customReadonly}
testId={testId}
/>
) : (
<InputsField
name={name}
defaultValue={defaultValue}
title={title}
tooltip={tooltip}
paramsTitle={paramsTitle}
expressionTitle={expressionTitle}
disabledTypes={disabledTypes}
onAppend={onAppend}
inputPlaceholder={inputPlaceholder}
literalDisabled={literalDisabled}
nameProps={nameProps}
customReadonly={customReadonly}
testId={testId}
/>
);

View File

@@ -0,0 +1,164 @@
/*
* 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 {
FieldArray,
type FieldArrayRenderProps,
} from '@flowgram-adapter/free-layout-editor';
import type { ViewVariableType, InputValueVO } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import { ColumnsTitleWithAction } from '@/form-extensions/components/columns-title-with-action';
import {
AddButton,
FieldArrayItem,
FieldRows,
Section,
type FieldProps,
} from '@/form';
import {
ValueExpressionInputField,
type ValueExpressionInputProps,
} from '../value-expression-input';
import { type NodeInputNameProps } from './node-input-name/type';
import { NodeInputNameField } from './node-input-name';
interface InputsFieldProps extends FieldProps<InputValueVO[]> {
title?: string;
paramsTitle?: string;
expressionTitle?: string;
disabledTypes?: ViewVariableType[];
onAppend?: () => InputValueVO;
inputPlaceholder?: string;
literalDisabled?: boolean;
showEmptyText?: boolean;
nthCannotDeleted?: number;
nameProps?: Partial<NodeInputNameProps>;
inputProps?: ValueExpressionInputProps;
customReadonly?: boolean;
testId?: string;
}
export const InputsField = ({
name,
defaultValue,
title,
tooltip,
paramsTitle,
expressionTitle,
disabledTypes,
inputPlaceholder,
literalDisabled,
onAppend,
nthCannotDeleted,
showEmptyText = true,
nameProps = {},
inputProps = {},
customReadonly,
testId,
}: InputsFieldProps) => {
const formReadonly = useReadonly();
const readonly = formReadonly || customReadonly;
return (
<FieldArray<InputValueVO> name={name} defaultValue={defaultValue}>
{({ field }: FieldArrayRenderProps<InputValueVO>) => {
const { value = [], delete: remove, append } = field;
const length = value?.length ?? 0;
const isEmpty = !length;
const disableRemove = nthCannotDeleted === length;
return (
<Section
title={title}
tooltip={tooltip}
actions={
!readonly
? [
<AddButton
dataTestId={`${testId}.add-button`}
onClick={() => {
const newValue = (onAppend?.() ?? {
name: '',
}) as InputValueVO;
append(newValue);
}}
/>,
]
: []
}
isEmpty={showEmptyText && isEmpty}
emptyText={I18n.t('workflow_inputs_empty')}
>
<ColumnsTitleWithAction
columns={[
{
title:
paramsTitle ??
I18n.t('workflow_detail_node_parameter_name'),
style: { flex: 2 },
},
{
title:
expressionTitle ??
I18n.t('workflow_detail_end_output_value'),
style: { flex: 3 },
},
]}
readonly={readonly}
className="mb-[8px]"
style={{
display: isEmpty ? 'none' : 'flex',
}}
/>
<FieldRows>
{field.map((item, index) => (
<FieldArrayItem
key={item.key}
disableRemove={disableRemove}
hiddenRemove={readonly}
onRemove={() => remove(index)}
>
<div style={{ flex: 2 }}>
<NodeInputNameField
name={`${item.name}.name`}
placeholder={I18n.t(
'workflow_detail_node_input_entername',
)}
input={item.value.input}
inputParameters={value}
{...nameProps}
/>
</div>
<div style={{ flex: 3 }}>
<ValueExpressionInputField
name={`${name}.${index}.input`}
disabledTypes={disabledTypes}
inputPlaceholder={inputPlaceholder}
literalDisabled={literalDisabled}
customReadonly={customReadonly}
{...inputProps}
/>
</div>
</FieldArrayItem>
))}
</FieldRows>
</Section>
);
}}
</FieldArray>
);
};

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 InputValueVO } from '@coze-workflow/base';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import { type ValidationProps } from '@/nodes-v2/components/validation/with-validation';
import { withValidation } from '@/nodes-v2/components/validation';
import {
InputTree,
type InputTreeProps,
} from '@/form-extensions/components/input-tree';
import { useField, withField, type FieldProps } from '@/form';
interface InputsTreeFieldProps extends FieldProps<InputValueVO[]> {
title?: string;
customReadonly?: boolean;
testId?: string;
}
const InputTreeWithValidation = withValidation(
(props: InputTreeProps & ValidationProps) => <InputTree {...props} />,
);
export const InputsTreeField = withField(
({ title, tooltip, customReadonly, testId }: InputsTreeFieldProps) => {
const { value, onChange, errors } = useField<InputValueVO[]>();
const formReadonly = useReadonly();
const readonly = formReadonly || customReadonly;
return (
<InputTreeWithValidation
value={value}
title={title}
titleTooltip={tooltip}
readonly={readonly}
onChange={onChange}
errors={errors}
testId={testId}
/>
);
},
{
hasFeedback: false,
},
);

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 { withField } from '@/form';
import { NodeInputName } from './node-input-name';
export const NodeInputNameField = withField(NodeInputName);

View File

@@ -0,0 +1,171 @@
/*
* 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 { useCallback, useEffect, useState } from 'react';
import classNames from 'classnames';
import {
useCurrentEntity,
useService,
} from '@flowgram-adapter/free-layout-editor';
import { WorkflowVariableFacadeService } from '@coze-workflow/variable';
import { type RefExpression, useNodeTestId } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Input, Tooltip, Typography } from '@coze-arch/coze-design';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import {
getUniqueName,
getVariableName,
} from '@/form-extensions/setters/node-input-name/utils';
import { useField } from '@/form';
import type { NodeInputNameProps } from './type';
export const NodeInputName = ({
style,
input,
inputParameters,
initValidate = false,
isPureText = false,
prefix = '',
suffix = '',
placeholder = I18n.t('workflow_detail_node_input_entername'),
format,
tooltip,
readonly: customReadonly,
}: NodeInputNameProps) => {
const { name, value, onChange, onBlur, errors } = useField<string>();
const [initialized, setInitialized] = useState<boolean>(false);
const [userEdited, setUserEdited] = useState<boolean>(false);
const [variableName, setVariableName] = useState<string | undefined>(value);
const [text, setText] = useState<string | undefined>(value);
const formReadonly = useReadonly();
const readonly = formReadonly || customReadonly;
const node = useCurrentEntity();
const variableService = useService(WorkflowVariableFacadeService);
const { getNodeSetterId } = useNodeTestId();
// text 状态受控(删除节点时联动 text 的值)
useEffect(() => {
if (value !== text) {
setText(value);
}
}, [value]);
const computedVariableName = getVariableName({
input: input as RefExpression,
prefix,
suffix,
format,
node,
variableService,
});
const onInputChange = useCallback((newInputValue: string): void => {
setUserEdited(true);
setText(newInputValue || '');
setVariableName(undefined);
}, []);
const handleOnBlur = () => {
onChange(text || '');
onBlur?.();
};
useEffect(() => {
if (initValidate) {
// 初始化写值触发校验
onChange(value as string);
}
if (value) {
setUserEdited(true);
}
setInitialized(true);
}, []);
if (initialized && !readonly && !userEdited) {
if (computedVariableName && computedVariableName !== variableName) {
const computedUniqueName = getUniqueName({
variableName: computedVariableName,
inputParameters,
});
onChange(computedUniqueName);
setVariableName(computedVariableName);
setText(computedUniqueName);
} else if (!computedVariableName && variableName) {
setVariableName(undefined);
setText(undefined);
}
}
return (
<div
className="flex flex-col items-start"
style={{
...style,
pointerEvents: readonly ? 'none' : 'auto',
}}
>
{isPureText ? (
<>
<Typography.Text className="h-8 leading-8 overflow-hidden whitespace-nowrap text-ellipsis">
{value}
</Typography.Text>
{tooltip ? (
<Tooltip content={tooltip}>
<IconCozInfoCircle
className="ml-1"
style={{
fontSize: 12,
}}
/>
</Tooltip>
) : null}
</>
) : (
<>
<Input
className={classNames({
'semi-input-wrapper-error': errors?.length,
})}
size={'small'}
data-testid={getNodeSetterId(name)}
value={text}
onChange={onInputChange}
onBlur={handleOnBlur}
validateStatus={errors?.length ? 'error' : undefined}
placeholder={placeholder}
readonly={readonly}
/>
{tooltip ? (
<Tooltip content={tooltip}>
<IconCozInfoCircle
className="ml-1"
style={{
fontSize: 12,
}}
/>
</Tooltip>
) : null}
</>
)}
</div>
);
};

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CSSProperties } from 'react';
import type { WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import type { InputValueVO, ValueExpression } from '@coze-workflow/base';
export type NodeInputNameFormat = (params: {
name: string;
prefix: string;
suffix: string;
input: ValueExpression;
node: WorkflowNodeEntity;
}) => string;
export interface NodeInputNameProps {
readonly?: boolean;
initValidate?: boolean;
isPureText?: boolean;
style?: CSSProperties;
/** 同一层的变量表达式 */
input: ValueExpression;
/** 当前输入列表中所有输入项 */
inputParameters: Array<InputValueVO>;
/** 前缀 */
prefix?: string;
/** 后缀 */
suffix?: string;
/** 名称自定义格式化 */
format?: NodeInputNameFormat;
tooltip?: string;
isError?: boolean;
placeholder?: string;
}

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 { LoopOutputsField } from './loop-outputs-field';

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.
*/
/* eslint-disable @typescript-eslint/no-empty-interface */
import { type RefExpression, useNodeTestId } from '@coze-workflow/base';
import { LoopOutputSelect } from '@/form-extensions/components/loop-output-select';
import { useField, withField } from '@/form';
interface LoopOutputSelectFieldProps {}
export const LoopOutputSelectField = withField<
LoopOutputSelectFieldProps,
RefExpression
>(() => {
const { name, value, onChange, readonly } = useField<RefExpression>();
const { getNodeSetterId } = useNodeTestId();
const testId = getNodeSetterId(name);
return (
<LoopOutputSelect
value={value}
onChange={onChange}
readonly={readonly}
testId={testId}
/>
);
});

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 {
FieldArray,
type FieldArrayRenderProps,
} from '@flowgram-adapter/free-layout-editor';
import { type InputValueVO } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import { NodeInputNameField } from '@/node-registries/common/fields/inputs-parameters-field/node-input-name';
import { ColumnsTitleWithAction } from '@/form-extensions/components/columns-title-with-action';
import {
AddButton,
FieldArrayItem,
FieldRows,
Section,
type FieldProps,
} from '@/form';
import { type NodeInputNameProps } from '../inputs-parameters-field/node-input-name/type';
import { LoopOutputSelectField } from './loop-output-select-field';
interface LoopOutputsFieldProps extends FieldProps<InputValueVO[]> {
title?: string;
tooltip?: string;
nameProps?: Partial<NodeInputNameProps>;
}
export const LoopOutputsField = ({
name,
defaultValue,
title,
tooltip,
nameProps = {},
}: LoopOutputsFieldProps) => {
const readonly = useReadonly();
return (
<FieldArray<InputValueVO> name={name} defaultValue={defaultValue}>
{({ field }: FieldArrayRenderProps<InputValueVO>) => {
const { value = [], delete: remove, append } = field;
return (
<Section
title={title}
tooltip={tooltip}
actions={
!readonly
? [
<AddButton
onClick={() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
append({} as InputValueVO);
}}
/>,
]
: []
}
isEmpty={!value || value?.length === 0}
emptyText={I18n.t('workflow_inputs_empty')}
>
<ColumnsTitleWithAction
columns={[
{
title: I18n.t('workflow_detail_node_parameter_name'),
style: {
flex: 2,
minWidth: 0,
},
},
{
title: I18n.t('workflow_detail_node_parameter_value'),
style: {
flex: 3,
minWidth: 0,
},
},
{
title: I18n.t('workflow_detail_start_variable_type'),
style: {
width: 90,
},
},
]}
readonly={readonly}
className="mb-[8px]"
/>
<FieldRows>
{field.map((item, index) => (
<FieldArrayItem
key={item.key}
hiddenRemove={readonly}
onRemove={() => remove(index)}
>
<div style={{ flex: 2 }}>
<NodeInputNameField
name={`${item.name}.name`}
placeholder={I18n.t(
'workflow_detail_node_input_entername',
)}
input={item.value.input}
inputParameters={value}
{...nameProps}
/>
</div>
<div style={{ flex: '3 1 90px', overflow: 'hidden' }}>
<LoopOutputSelectField name={`${item.name}.input`} />
</div>
</FieldArrayItem>
))}
</FieldRows>
</Section>
);
}}
</FieldArray>
);
};

View File

@@ -0,0 +1,43 @@
/*
* 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 React from 'react';
import type { IModelValue } from '@/typing';
import { Section, useField, withField } from '@/form';
import { ModelSelect } from '@/components/model-select';
function ModelSelectComp({
title,
tooltip,
}: {
title?: string;
tooltip?: string;
}) {
const { value, onChange, readonly, name } = useField<IModelValue>();
return (
<Section title={title} tooltip={tooltip}>
<ModelSelect
name={name}
value={value}
onChange={e => onChange(e as IModelValue)}
readonly={readonly}
/>
</Section>
);
}
export const ModelSelectField = withField(ModelSelectComp);

View File

@@ -0,0 +1,63 @@
/*
* 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 {
VARIABLE_TYPE_ALIAS_MAP,
type ViewVariableType,
} from '@coze-workflow/base';
import { OutputsParamDisplay } from '@/form-extensions/components/output-param-display';
import { withField, useField } from '@/form';
export interface OutputsProps {
id: string;
name: string;
settingOnErrorPath?: string;
topLevelReadonly?: boolean;
disabledTypes?: ViewVariableType[];
title?: string;
tooltip?: React.ReactNode;
disabled?: boolean;
customReadonly?: boolean;
hiddenTypes?: ViewVariableType[];
noCard?: boolean;
jsonImport?: boolean;
allowAppendRootData?: boolean;
withDescription?: boolean;
withRequired?: boolean;
}
export const OutputsDisplayField = withField<OutputsProps>(() => {
const { value } = useField<
{
name: string;
type: string;
required?: boolean;
}[]
>();
return (
<OutputsParamDisplay
options={{
outputInfo: value?.map(item => ({
...item,
label: item.name ?? '',
type: VARIABLE_TYPE_ALIAS_MAP[item.type],
})),
}}
/>
);
});

View File

@@ -0,0 +1,19 @@
/*
* 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 { OutputsField } from './outputs';
export { provideNodeOutputVariablesEffect } from '@/nodes-v2/materials/provide-node-output-variables';
export { outputTreeMetaValidator } from './output-tree-meta-validator';

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import { type ZodError } from 'zod';
import { get } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { type Validate } from '@flowgram-adapter/free-layout-editor';
/** 变量命名校验规则 */
const outputTreeValidationRule =
/^(?!.*\b(true|false|and|AND|or|OR|not|NOT|null|nil|If|Switch)\b)[a-zA-Z_][a-zA-Z_$0-9]*$/;
/** 校验逻辑 */
// eslint-disable-next-line @typescript-eslint/naming-convention
const OutputTreeMetaSchema = z.lazy(() =>
z
.object({
name: z
.string({
required_error: I18n.t('workflow_detail_node_error_name_empty'),
})
.min(1, I18n.t('workflow_detail_node_error_name_empty'))
.regex(
outputTreeValidationRule,
I18n.t('workflow_detail_node_error_format'),
),
children: z.array(OutputTreeMetaSchema).optional(),
})
.passthrough(),
);
const omitErrorBody = (value, isBatch) => {
// 批量,去除 children 中的 errorBody
if (isBatch) {
return value?.map(v => ({
...v,
children: v?.children?.filter(c => c?.name !== 'errorBody'),
}));
}
// 单次,去除 value 中的 errorBody
return value?.filter(v => v?.name !== 'errorBody');
};
export const outputTreeMetaValidator: Validate = ({ value, formValues }) => {
/**
* 判断错误异常处理是否打开,如果打开需要过滤掉 errorBody 后做校验
*/
const { settingOnErrorIsOpen = false } = (get(formValues, 'settingOnError') ??
{}) as { settingOnErrorIsOpen?: boolean };
/**
* 根据 batch 数据判断,当前是否处于批处理状态
*/
const batchValue = get(formValues, 'batchMode');
const isBatch = batchValue === 'batch';
const parsed = z
.array(OutputTreeMetaSchema)
.safeParse(settingOnErrorIsOpen ? omitErrorBody(value, isBatch) : value);
if (!parsed.success) {
const errorText = JSON.stringify((parsed as { error: ZodError }).error);
return errorText;
}
return undefined;
};

View File

@@ -0,0 +1,110 @@
/*
* 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 ViewVariableType } from '@coze-workflow/base';
import { Outputs } from '@/nodes-v2/components/outputs';
import { withField, useField, useWatch } from '@/form';
export interface OutputsProps {
id?: string;
name: string;
settingOnErrorPath?: string;
topLevelReadonly?: boolean;
disabledTypes?: ViewVariableType[];
title?: string;
tooltip?: React.ReactNode;
disabled?: boolean;
disabledTooltip?: string;
customReadonly?: boolean;
hiddenTypes?: ViewVariableType[];
noCard?: boolean;
jsonImport?: boolean;
allowAppendRootData?: boolean;
withDescription?: boolean;
withRequired?: boolean;
addItemTitle?: string;
allowDeleteLast?: boolean;
emptyPlaceholder?: string;
defaultCollapse?: boolean;
batchMode?: string;
needAppendChildWhenNodeIsPreset?: boolean;
/**
* 是否可以配置默认值
*/
withDefaultValue?: boolean;
/**
* 默认展开的参数名
*/
defaultExpandParams?: string[];
/**
* 列宽比 如 6:4 代表名称占6份类型占4份
*/
columnsRatio?: string;
maxLimit?: number;
}
export const OutputsField = withField<OutputsProps>(
({
id = 'outputs',
topLevelReadonly = false,
settingOnErrorPath = 'settingOnError.settingOnErrorIsOpen',
disabledTypes = [],
hiddenTypes,
title,
tooltip,
disabled,
customReadonly = false,
noCard,
jsonImport,
allowAppendRootData,
withDescription = true,
withRequired,
batchMode,
...props
}: OutputsProps) => {
const { value, onChange, readonly, onBlur, errors } = useField();
const settingOnErrorIsOpen = useWatch(settingOnErrorPath);
return (
<Outputs
id={id}
value={value}
onChange={v => {
onChange?.(v);
// 保证 blur 时触发校验, 直接传 onBlur 不生效
onBlur?.();
}}
title={title}
titleTooltip={tooltip}
topLevelReadonly={topLevelReadonly}
disabledTypes={disabledTypes}
hiddenTypes={hiddenTypes}
readonly={readonly || customReadonly}
disabled={disabled}
needErrorBody={settingOnErrorIsOpen}
noCard={noCard}
batchMode={batchMode}
jsonImport={jsonImport}
allowAppendRootData={allowAppendRootData}
withDescription={withDescription}
withRequired={withRequired}
errors={errors}
{...props}
/>
);
},
);

View File

@@ -0,0 +1,7 @@
.parameters-title {
width: calc(100% - 12px);
}
.parameters-title-readonly {
width: calc(100% - 32px);
}

Some files were not shown because too many files have changed in this diff Show More