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,375 @@
/*
* 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 { get, isNil } from 'lodash-es';
import {
Field,
type FieldArrayRenderProps,
FieldArray,
type FieldRenderProps,
type FormMetaV2,
type FormRenderProps,
ValidateTrigger,
} from '@flowgram-adapter/free-layout-editor';
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
import {
PublicScopeProvider,
type RefExpression,
type ValueExpression,
type ValueExpressionDTO,
ValueExpressionType,
variableUtils,
ViewVariableType,
} from '@coze-workflow/variable';
import { concatTestId } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { IconCozPlus, IconCozMinus } from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
import { provideNodeOutputVariablesEffect } from '@/nodes-v2/materials/provide-node-output-variables';
import { fireNodeTitleChange } from '@/nodes-v2/materials/fire-node-title-change';
import { createValueExpressionInputValidate } from '@/nodes-v2/materials/create-value-expression-input-validate';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import { OutputSingleText } from '@/nodes-v2/components/output-single-text';
import { GlobalVariableSelect } from '@/nodes-v2/components/global-variable-select';
import { FormItemFeedback } from '@/nodes-v2/components/form-item-feedback';
import { useVariableService } from '@/hooks';
import { type VariableMetaWithNode } from '@/form-extensions/typings';
import { FormCard } from '@/form-extensions/components/form-card';
import { ColumnsTitleWithAction } from '@/form-extensions/components/columns-title-with-action';
import { nodeMetaValidate } from '../materials/node-meta-validate';
import { ValueExpressionInput } from '../components/value-expression-input';
import NodeMeta from '../components/node-meta';
import styles from './index.module.less';
function getInputParametersHeaderColumns() {
return [
{
title: I18n.t('workflow_detail_variable_input_name'),
style: {
flex: 2,
},
},
{
title: I18n.t('workflow_detail_variable_input_value'),
style: {
flex: 3,
},
},
];
}
interface InputValueVO {
left: ValueExpression;
right: ValueExpression;
}
interface FormData {
$$input_decorator$$: {
inputParameters?: InputValueVO[];
};
}
const getDefaultInputValue = () => ({
left: { type: ValueExpressionType.REF },
right: { type: ValueExpressionType.REF },
});
const Render = ({ form }: FormRenderProps<FormData>) => {
const readonly = useReadonly();
const variableService = useVariableService();
const node = useCurrentEntity();
const getFieldType = (valuePath: string) => {
const value = form.getValueIn(valuePath);
return variableUtils.getValueExpressionViewType(value, variableService, {
node,
});
};
const genGlobalVariablesFilter = (valuePath: string) => {
const currentValue: RefExpression = form.getValueIn(valuePath) || {};
const selectedValue: InputValueVO[] =
form.getValueIn('$$input_decorator$$.inputParameters') || [];
return (variables: VariableMetaWithNode[]) => {
const currentValueKeyPathStr = currentValue?.content?.keyPath?.join('.');
const selectedValueKeyPathStrArr = selectedValue
.map(item => (item.left as RefExpression)?.content?.keyPath?.join('.'))
.filter(keyPathStr => currentValueKeyPathStr !== keyPathStr);
return variables
.filter(item => !item.readonly)
.filter(
item =>
!selectedValueKeyPathStrArr.includes(
[item.nodeId, item.key].join('.'),
),
);
};
};
return (
<PublicScopeProvider>
<>
<NodeMeta deps={['outputs']} outputsPath={'outputs'} />
<FieldArray
name={'$$input_decorator$$.inputParameters'}
defaultValue={[getDefaultInputValue()]}
>
{({ field: fieldArray }: FieldArrayRenderProps<InputValueVO>) => (
<FormCard
header={I18n.t('workflow_detail_node_input')}
tooltip={I18n.t('workflow_detail_variable_subtitle')}
>
<div className={styles['columns-title']}>
<ColumnsTitleWithAction
columns={getInputParametersHeaderColumns()}
/>
</div>
{fieldArray.map((child, index) => (
<div
key={child.key}
style={{
display: 'flex',
alignItems: 'flex-start',
paddingBottom: 4,
gap: 4,
}}
>
<Field name={`${child.name}.left`}>
{({
field: childNameField,
fieldState: nameFieldState,
}: FieldRenderProps<RefExpression | undefined>) => (
<div
style={{
flex: 2,
minWidth: 0,
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
}}
>
<GlobalVariableSelect
{...childNameField}
variablesFilter={genGlobalVariablesFilter(
`${child.name}.left`,
)}
isError={!!nameFieldState?.errors?.length}
disabled={readonly}
useMatchType={
!childNameField.value?.content?.keyPath?.[0]
}
matchType={getFieldType(`${child.name}.right`)}
/>
<FormItemFeedback errors={nameFieldState?.errors} />
</div>
)}
</Field>
<Field name={`${child.name}.right`}>
{({
field: childInputField,
fieldState: inputFieldState,
}: FieldRenderProps<ValueExpression | undefined>) => {
const matchLeftType = getFieldType(`${child.name}.left`);
return (
<div style={{ flex: 3, overflow: 'hidden' }}>
<ValueExpressionInput
{...childInputField}
inputType={matchLeftType}
matchType={matchLeftType}
isError={!!inputFieldState?.errors?.length}
readonly={readonly}
/>
<FormItemFeedback errors={inputFieldState?.errors} />
</div>
);
}}
</Field>
{readonly ? (
<></>
) : (
<div>
<IconButton
size="small"
color="secondary"
data-testid={concatTestId(child.name, 'remove')}
icon={<IconCozMinus />}
onClick={() => {
fieldArray.delete(index);
}}
/>
</div>
)}
</div>
))}
{readonly ? (
<></>
) : (
<div className={styles['input-add-icon']}>
<IconButton
className="!block"
color="highlight"
size="small"
icon={<IconCozPlus />}
onClick={() => {
fieldArray.append(getDefaultInputValue());
}}
/>
</div>
)}
</FormCard>
)}
</FieldArray>
<Field name={'outputs'}>
<FormCard
header={I18n.t('workflow_detail_node_output')}
tooltip={I18n.t('workflow_detail_variable_set_output_tooltip')}
>
<OutputSingleText label={'isSuccess'} type={'boolean'} />
</FormCard>
</Field>
</>
</PublicScopeProvider>
);
};
export const VARIABLE_ASSIGN_FORM_META: FormMetaV2<FormData> = {
render: props => <Render {...props} />,
validateTrigger: ValidateTrigger.onChange,
validate: {
nodeMeta: nodeMetaValidate,
'$$input_decorator$$.inputParameters.*.left':
createValueExpressionInputValidate({
required: true,
emptyErrorMessage: I18n.t('variable_assignment_node_select_empty'),
}),
'$$input_decorator$$.inputParameters.*.right':
createValueExpressionInputValidate({ required: true }),
},
effect: {
nodeMeta: fireNodeTitleChange,
outputs: provideNodeOutputVariablesEffect,
},
formatOnInit(value, context) {
const inputParameters = get(value, 'inputs.inputParameters');
const inputParameterDTOs: {
left: ValueExpressionDTO;
input: ValueExpressionDTO;
}[] = inputParameters ?? [];
const inputParameterVOs: {
left: ValueExpression;
right: ValueExpression;
}[] = inputParameterDTOs
.map(inputParameterDTO => {
const leftVariableDTO = inputParameterDTO?.left;
const rightVariableDTO = inputParameterDTO?.input;
if (!leftVariableDTO || !rightVariableDTO) {
return;
}
const leftVariableVO = variableUtils.valueExpressionToVO(
leftVariableDTO,
context.playgroundContext.variableService,
);
const rightVariableVO = variableUtils.valueExpressionToVO(
rightVariableDTO,
context.playgroundContext.variableService,
);
return {
left: leftVariableVO,
right: rightVariableVO,
};
})
.filter(Boolean) as {
left: ValueExpression;
right: ValueExpression;
}[];
const initValue = {
nodeMeta: value?.nodeMeta,
$$input_decorator$$: {
inputParameters: inputParameterVOs?.length
? inputParameterVOs
: [getDefaultInputValue()],
},
outputs: [{ name: 'isSuccess', type: ViewVariableType.Boolean }],
};
return initValue;
},
formatOnSubmit(value, context) {
const inputParameters = get(value, '$$input_decorator$$.inputParameters');
const formattedValue: Record<string, unknown> = {
nodeMeta: value?.nodeMeta || {},
inputs: {
inputParameters: inputParameters?.map(input => {
const leftVariableVO = input?.left as RefExpression;
const rightVariableVO = input?.right as RefExpression;
const leftVariableDTO = variableUtils.valueExpressionToDTO(
leftVariableVO,
context.playgroundContext.variableService,
{ node: context.node },
);
const rightVariableDTO = variableUtils.valueExpressionToDTO(
rightVariableVO,
context.playgroundContext.variableService,
{ node: context.node },
);
return {
name: leftVariableVO?.content?.keyPath?.slice(1).join('.'),
left: leftVariableVO?.content ? leftVariableDTO : undefined,
input: !isNil(rightVariableVO?.content)
? rightVariableDTO
: undefined,
};
}),
variableTypeMap: inputParameters.reduce((acc, cur) => {
const type = cur?.left?.content?.keyPath?.[0];
const key = cur?.left?.content?.keyPath?.slice(1).join('.');
if (!acc[key]) {
acc[key] = type;
}
return acc;
}, {}),
},
outputs: [{ name: 'isSuccess', type: 'boolean' }],
};
return formattedValue;
},
};

View File

@@ -0,0 +1,9 @@
.input-add-icon {
position: absolute;
top: 10px;
right: 12px;
}
.columns-title{
padding-bottom: 8px;
}

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 { VARIABLE_ASSIGN_NODE_REGISTRY } from './node-registry';

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DEFAULT_NODE_META_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 { VARIABLE_ASSIGN_FORM_META } from './form-meta';
export const VARIABLE_ASSIGN_NODE_REGISTRY: WorkflowNodeRegistry<NodeTestMeta> =
{
type: StandardNodeType.VariableAssign,
meta: {
nodeDTOType: StandardNodeType.VariableAssign,
style: {
width: 360,
},
size: { width: 360, height: 130.7 },
nodeMetaPath: DEFAULT_NODE_META_PATH,
inputParametersPath: '/$$input_decorator$$/inputParameters',
test,
helpLink: '/open/docs/guides/variable_assign_node',
},
variablesMeta: {
outputsPathList: [],
inputsPathList: [],
},
formMeta: VARIABLE_ASSIGN_FORM_META,
};

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 { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import {
generateParametersToProperties,
generateEnvToRelatedContextProperties,
} from '@/test-run-kit';
import { type NodeTestMeta } from '@/test-run-kit';
export const test: NodeTestMeta = {
generateRelatedContext(node, context) {
const { isInProject } = context;
if (isInProject) {
return {};
}
return generateEnvToRelatedContextProperties({
isNeedBot: true,
hasVariableAssignNode: true,
});
},
generateFormInputProperties(node) {
const formData = node
.getData(FlowNodeFormData)
.formModel.getFormItemValueByPath('/');
const parameters = formData?.$$input_decorator$$?.inputParameters?.map(
e => ({
input: e?.right,
name: e?.left?.content?.keyPath?.[1],
}),
);
return generateParametersToProperties(parameters, { node });
},
};