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,3 @@
.option-expression-editor {
background-color: transparent;
}

View File

@@ -0,0 +1,56 @@
/*
* 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 { INTENT_NODE_MODE } from '@coze-workflow/nodes';
import type { InputValueVO } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import {
INTENT_MODE,
SYSTEM_PROMPT,
INPUT_PATH,
} from '@/node-registries/intent/constants';
import { ExpressionEditorField } from '@/node-registries/common/fields';
import { Section, useWatch } from '@/form';
import styles from './index.module.less';
export default function AdvancedSetting() {
const intentMode = useWatch({ name: INTENT_MODE });
const isShow = intentMode === INTENT_NODE_MODE.STANDARD;
const inputParameters = useWatch<InputValueVO[]>(INPUT_PATH);
return (
isShow && (
<Section
title={I18n.t('workflow_LLM_node_sp_title')}
tooltip={I18n.t('workflow_intent_advance_set_tooltips')}
>
<ExpressionEditorField
className={'!p-[4px]'}
required={false}
name={SYSTEM_PROMPT}
placeholder={I18n.t('workflow_intent_advance_set_placeholder')}
inputParameters={inputParameters}
testId={`/${SYSTEM_PROMPT.split('.').join('/')}`}
containerClassName={styles['option-expression-editor']}
/>
</Section>
)
);
}

View File

@@ -0,0 +1,15 @@
.line {
height: 1px;
margin-top: -3px;
margin-bottom: 14px;
background-color: #FFF;
}
/* stylelint-disable-next-line plugin/disallow-first-level-global */
:global(.llm-inputs-wrapper .custom-action-button) {
margin-right: 32px;
}
.chat-history-text {
font-size: 12px;
}

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 { nanoid } from 'nanoid';
import { ViewVariableType } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { Switch, Tooltip } from '@coze-arch/coze-design';
import { INPUT_CHAT_HISTORY_SETTING_ENABLE } from '@/node-registries/intent/constants';
import {
OutputTree,
type OutputTreeProps,
} from '@/form-extensions/components/output-tree';
import { withField, useField, useWatch } from '@/form';
import { ChatHistoryRound } from '@/components/chat-history-round';
import styles from './index.module.less';
export interface SwitchFieldProps {
testId?: string;
}
export const ChatHistoryEnableSwitch = withField<SwitchFieldProps, boolean>(
({ testId }) => {
const { value, onChange, readonly } = useField<boolean>();
return (
<Tooltip content={I18n.t('wf_chatflow_125')} position="right">
<div className="flex items-center gap-1">
<div className={styles['chat-history-text']}>
{I18n.t('wf_chatflow_124')}
</div>
<Switch
data-testid={testId}
disabled={readonly}
size="mini"
checked={value}
onChange={onChange}
/>
</div>
</Tooltip>
);
},
);
const VALUE = [
{
key: nanoid(),
name: 'chatHistory',
type: ViewVariableType.ArrayObject,
children: [
{
key: nanoid(),
name: 'role',
type: ViewVariableType.String,
},
{
key: nanoid(),
name: 'content',
type: ViewVariableType.String,
},
],
},
] as OutputTreeProps['value'];
export const ChatHistoryPanel = withField(() => {
const enableChatHistory = useWatch({
name: INPUT_CHAT_HISTORY_SETTING_ENABLE,
});
const { value, onChange, readonly } = useField<number>();
return enableChatHistory ? (
<div className="relative">
<OutputTree
id="chat-history"
readonly
value={VALUE}
defaultCollapse
onChange={() => {
console.log('OutputTree change');
}}
withDescription={false}
withRequired={false}
noCard
/>
<div className={styles.line} />
<ChatHistoryRound
value={value}
readonly={readonly}
onChange={w => {
onChange(Number(w));
}}
/>
</div>
) : null;
});

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { type InputValueVO, useNodeTestId } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { ValueExpressionInputField } from '@/node-registries/common/fields';
import { useGlobalState } from '@/hooks';
import {
ColumnTitles,
FieldArrayItem,
FieldArrayList,
Section,
useFieldArray,
withFieldArray,
} from '@/form';
import {
INPUT_CHAT_HISTORY_SETTING_ENABLE,
INPUT_CHAT_HISTORY_SETTING_ROUND,
COLUMNS,
INPUT_PATH,
} from '../../constants';
import { ChatHistoryEnableSwitch, ChatHistoryPanel } from './chat-history';
const InputsParametersField = withFieldArray(() => {
const { name: fieldName, value } = useFieldArray<InputValueVO>();
const safeValue = value || [];
const { getNodeSetterId } = useNodeTestId();
const { isChatflow } = useGlobalState();
return (
<Section
title={I18n.t('workflow_detail_node_parameter_input')}
tooltip={I18n.t('workflow_intent_input_tooltips')}
testId={getNodeSetterId(fieldName)}
actions={
isChatflow
? [
<ChatHistoryEnableSwitch
name={INPUT_CHAT_HISTORY_SETTING_ENABLE}
testId={getNodeSetterId('chatHistorySetting')}
/>,
]
: []
}
>
{isChatflow ? (
<ChatHistoryPanel name={INPUT_CHAT_HISTORY_SETTING_ROUND} />
) : null}
<ColumnTitles columns={COLUMNS} />
<FieldArrayList>
{safeValue?.map(({ name }, index) => (
<FieldArrayItem hiddenRemove>
<ValueExpressionInputField
key={index}
label={name}
required
name={`${fieldName}.${index}.input`}
/>
</FieldArrayItem>
))}
</FieldArrayList>
</Section>
);
});
export default function InputsParameters() {
return (
<InputsParametersField
name={INPUT_PATH}
defaultValue={[{ name: 'query' }]}
/>
);
}

View File

@@ -0,0 +1,15 @@
.expression-editor-no-interpolation {
:global {
.cm-editor .cm-decoration-interpolation-valid {
/* stylelint-disable-next-line declaration-no-important */
color: inherit !important;
/* stylelint-disable-next-line declaration-no-important */
caret-color: inherit !important;
}
}
}
.option-expression-editor {
background-color: transparent;
}

View File

@@ -0,0 +1,268 @@
/*
* 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 { isNil } from 'lodash-es';
import update from 'immutability-helper';
import classNames from 'classnames';
import {
MINIMAL_INTENT_ITEMS,
STANDARD_INTENT_ITEMS,
INTENT_NODE_MODE,
} from '@coze-workflow/nodes';
import { useNodeTestId } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { IconCozPlus } from '@coze-arch/coze-design/icons';
import { IconButton, Tooltip } from '@coze-arch/coze-design';
import {
INTENT_MODE,
INTENTS,
QUICK_INTENTS,
} from '@/node-registries/intent/constants';
import { useUpdateSortedPortLines } from '@/hooks';
import {
calcPortId,
convertNumberToLetters,
} from '@/form-extensions/setters/answer-option/utils';
import { useField, withField, Section, useWatch } from '@/form';
import { SortableList } from '@/components/sortable-list';
import { type IntentsType } from '../../types';
import { OptionItem } from './option-item';
interface Props {
/** 是否展示标题行 */
showTitleRow?: boolean;
/** 是否展示选项标签 */
showOptionName?: boolean;
/** 选项 placeholder */
optionPlaceholder?: string;
/** 默认分支名称 */
defaultOptionText?: string;
/** 选项是否允许插值 */
optionEnableInterpolation?: boolean;
/** 选项最大数量限制,默认值为整数最大值 */
maxItems?: number;
/** 新增按钮样式 */
addButtonClassName?: string;
/** 展示禁止添加 Tooltip */
showDisableAddTooltip?: boolean;
customDisabledAddTooltip?: string;
customClassName?: string;
}
const IntentsField = withField((props: Props) => {
const { value, onChange, readonly, name } = useField<IntentsType>();
const {
showTitleRow = true,
showOptionName = true,
optionPlaceholder = '',
defaultOptionText = '',
optionEnableInterpolation,
maxItems = Number.MAX_SAFE_INTEGER,
addButtonClassName = '',
showDisableAddTooltip = true,
customDisabledAddTooltip,
customClassName,
} = props;
const { getNodeSetterId } = useNodeTestId();
const handleChange = val => {
onChange(val);
};
const updateSortedPortLines = useUpdateSortedPortLines(calcPortId);
const onItemDelete = (index: number) => {
// 将要被删除的端口移动到最后,这样删除时不会对其他连线顺序产生影响
updateSortedPortLines(index, value.length);
const newVal = update(value, { $splice: [[index, 1]] });
handleChange(newVal);
};
const onItemChange = (val: string, index: number) => {
const newVal = update(value, {
[index]: {
name: { $set: val },
},
});
handleChange(newVal);
};
const AddActionButton = (
<IconButton
className={classNames('absolute right-3 top-3', {
[addButtonClassName]: addButtonClassName,
})}
color="highlight"
size="small"
disabled={readonly || value.length >= maxItems}
data-testid={getNodeSetterId('answer-option-add-btn')}
// 解决 Button 位移导致 onClick 不触发问题
onMouseDown={() => {
handleChange([...value, { name: '' }]);
}}
icon={<IconCozPlus className="text-sm" />}
/>
);
return (
<div
className={
!isNil(customClassName)
? classNames({
[customClassName]: customClassName,
})
: undefined
}
>
{showTitleRow ? (
<div className="flex items-center text-xs text-[#1D1C23] opacity-60 h-7">
<div className={'ml-1 w-[71px]'}>
{I18n.t('workflow_ques_ans_type_option_title', {}, 'options')}
</div>
<div className={'ml-6'}>
{I18n.t('workflow_ques_ans_type_option_content', {}, 'content')}
</div>
</div>
) : null}
<SortableList
value={value}
onChange={handleChange}
onDragEnd={updateSortedPortLines}
renderItem={(item, index, dragOption) => {
const { dragRef, isPreview } = dragOption || {};
return (
<OptionItem
name={`${name}.${index}`}
testIdPath={`/${name}`}
ref={dragRef}
index={index}
content={item.name}
onChange={val => onItemChange(val, index)}
portId={isPreview ? undefined : calcPortId(index)}
optionName={convertNumberToLetters(index)}
onDelete={!isPreview ? () => onItemDelete(index) : undefined}
disableDelete={value.length <= 1}
canDrag
readonly={readonly}
showOptionName={showOptionName}
optionPlaceholder={optionPlaceholder}
optionEnableInterpolation={optionEnableInterpolation}
isField
/>
);
}}
/>
<OptionItem
className="mt-2"
content={defaultOptionText}
optionName={I18n.t('workflow_ques_ans_type_option_other', {}, 'other')}
portId="default"
readonly
showOptionName={showOptionName}
optionPlaceholder={optionPlaceholder}
/>
<div className="mt-2 answer-option-add-button">
{showDisableAddTooltip && value.length >= maxItems ? (
<Tooltip
content={
customDisabledAddTooltip ||
I18n.t('workflow_250117_05', { maxCount: maxItems })
}
>
<div className="absolute right-3 top-3 background w-[24px] h-[24px] coz-mg-hglt coz-fg-hglt-dim p-[5px] rounded-[5px] flex items-center cursor-not-allowed">
<IconCozPlus className="text-sm" />
</div>
</Tooltip>
) : (
AddActionButton
)}
</div>
</div>
);
});
export const Intents = () => {
const intentMode = useWatch({ name: INTENT_MODE });
const isShow = intentMode === INTENT_NODE_MODE.STANDARD;
const { getNodeSetterId } = useNodeTestId();
return (
isShow && (
<Section
title={I18n.t('workflow_intent_matchlist_title')}
tooltip={I18n.t('workflow_intent_matchlist_tooltips')}
testId={getNodeSetterId(`/${INTENTS}`)}
>
<IntentsField
name={INTENTS}
showTitleRow={false}
showOptionName={false}
optionPlaceholder={I18n.t('workflow_intent_matchlist_placeholder')}
defaultOptionText={I18n.t('workflow_intent_matchlist_else')}
optionEnableInterpolation={false}
maxItems={STANDARD_INTENT_ITEMS}
customDisabledAddTooltip={I18n.t('workflow_250117_02')}
hasFeedback={false}
/>
</Section>
)
);
};
export const QuickIntents = () => {
const intentMode = useWatch({ name: INTENT_MODE });
const isShow = intentMode === INTENT_NODE_MODE.MINIMAL;
const { getNodeSetterId } = useNodeTestId();
return (
isShow && (
<Section
testId={getNodeSetterId(`/${QUICK_INTENTS}`)}
title={I18n.t('workflow_intent_matchlist_title')}
tooltip={I18n.t('workflow_intent_matchlist_tooltips')}
>
<IntentsField
name={QUICK_INTENTS}
showTitleRow={false}
showOptionName={false}
optionPlaceholder={I18n.t('workflow_intent_matchlist_placeholder')}
defaultOptionText={I18n.t('workflow_intent_matchlist_else')}
optionEnableInterpolation={false}
maxItems={MINIMAL_INTENT_ITEMS}
customDisabledAddTooltip={I18n.t('workflow_250117_01')}
hasFeedback={false}
/>
</Section>
)
);
};

View File

@@ -0,0 +1,189 @@
/*
* 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, { forwardRef } from 'react';
import classNames from 'classnames';
import { type FieldError } from '@flowgram-adapter/free-layout-editor';
import { IconHandle } from '@douyinfe/semi-icons';
import { useNodeTestId } from '@coze-workflow/base';
import { IconCozMinusCircle } from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
import { FormItemFeedback } from '@/nodes-v2/components/form-item-feedback';
import { ExpressionEditor } from '@/nodes-v2/components/expression-editor';
import { useField, withField } from '@/form';
import styles from '../index.module.less';
export interface OptionItemProps {
index?: number;
optionName: string;
content: string;
portId?: string;
canDrag?: boolean;
readonly?: boolean;
className?: string;
disableDelete?: boolean;
showOptionName?: boolean;
optionPlaceholder?: string;
optionEnableInterpolation?: boolean;
onChange?: (val: string) => void;
onDelete?: () => void;
errors?: FieldError[];
name?: string;
onFocus?: () => void;
onBlur?: () => void;
isField?: boolean;
testIdPath?: string;
}
const Item = (props: OptionItemProps) => {
const {
content,
portId,
readonly = false,
disableDelete,
onChange,
onDelete,
optionPlaceholder,
optionEnableInterpolation,
errors,
onFocus,
onBlur,
testIdPath,
name = '',
} = props;
const { getNodeSetterId, concatTestId } = useNodeTestId();
return (
<div className="w-full">
<div className="flex items-center space-x-1 w-full min-h-[24px]">
{!readonly ? (
<ExpressionEditor
name={name}
testId={testIdPath}
value={content}
onChange={val => {
onChange?.(val);
}}
onFocus={() => onFocus?.()}
onBlur={() => onBlur?.()}
isError={errors && errors?.length > 0}
readonly={readonly}
minRows={1}
placeholder={optionPlaceholder}
disableSuggestion={!optionEnableInterpolation}
className="!p-[4px]"
containerClassName={classNames(
'!rounded-[6px]',
styles['option-expression-editor'],
!optionEnableInterpolation
? styles['expression-editor-no-interpolation']
: '',
)}
/>
) : (
<div className="w-full">{content}</div>
)}
<div>
{onDelete && !readonly ? (
<IconButton
size="small"
color="secondary"
data-testid={concatTestId(
getNodeSetterId('answer-option-item-delete'),
portId || '',
)}
className={classNames('flex', {
'cursor-pointer': !disableDelete,
'cursor-not-allowed': disableDelete,
'text-[--semi-color-tertiary-active]': disableDelete,
})}
onClick={() => {
if (disableDelete) {
return;
}
onDelete();
}}
icon={<IconCozMinusCircle className="text-sm" />}
/>
) : null}
</div>
</div>
<FormItemFeedback errors={errors} />
</div>
);
};
const ItemField = withField((props: OptionItemProps) => {
const { onBlur, onFocus, errors } = useField();
return <Item {...props} errors={errors} onFocus={onFocus} onBlur={onBlur} />;
});
export const OptionItem = forwardRef<HTMLDivElement, OptionItemProps>(
(props, dragRef) => {
const {
canDrag,
portId,
readonly = false,
className,
optionName,
showOptionName,
isField,
name,
} = props;
const { getNodeSetterId, concatTestId } = useNodeTestId();
return (
<div
className={classNames('flex items-start space-x-1 text-xs', className)}
>
<div className="flex w-4 min-w-4 shrink-0 mt-[4px]">
{canDrag ? (
<IconHandle
data-testid={concatTestId(
getNodeSetterId('answer-option-item-handle'),
portId || '',
)}
data-disable-node-drag="true"
className={classNames({
'cursor-move': !readonly,
'pointer-events-none': readonly,
})}
ref={dragRef}
style={{ color: '#aaa' }}
/>
) : null}
</div>
{showOptionName ? (
<div className="break-keep min-w-[75px] mt-[5px]">{optionName}</div>
) : null}
{isField && name ? (
<ItemField {...props} name={name} hasFeedback={false} />
) : (
<Item {...props} />
)}
</div>
);
},
);

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { INTENT_NODE_MODE } from '@coze-workflow/nodes';
import { I18n } from '@coze-arch/i18n';
import { INTENT_MODE } from '@/node-registries/intent/constants';
import { RadioSetterField } from '@/node-registries/common/fields';
export default function ModeRadio() {
// The community version does not support the fast mode of intent recognition for future expansion
if (IS_OPEN_SOURCE) {
return null;
}
return (
<RadioSetterField
required
name={INTENT_MODE}
defaultValue={INTENT_NODE_MODE.MINIMAL}
options={{
key: 'questionParams.answer_type',
options: [
{
label: I18n.t('workflow_250117_03'),
value: INTENT_NODE_MODE.MINIMAL,
},
{
label: I18n.t('workflow_250117_04'),
value: INTENT_NODE_MODE.STANDARD,
},
],
}}
/>
);
}

View File

@@ -0,0 +1,39 @@
/*
* 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 { INTENT_NODE_MODE } from '@coze-workflow/nodes';
import { I18n } from '@coze-arch/i18n';
import { INTENT_MODE, MODEL } from '@/node-registries/intent/constants';
import { ModelSelectField } from '@/node-registries/common/fields';
import { useWatch } from '@/form';
export default function ModelSelect() {
const intentMode = useWatch({ name: INTENT_MODE });
const isShow = intentMode === INTENT_NODE_MODE.STANDARD;
return (
isShow && (
<ModelSelectField
required
name={MODEL}
title={I18n.t('workflow_detail_llm_model')}
/>
)
);
}

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 React from 'react';
import { I18n } from '@coze-arch/i18n';
import { OutputsField } from '@/node-registries/common/fields';
export default function Outputs() {
return (
<OutputsField
title={I18n.t('workflow_detail_node_output')}
tooltip={I18n.t('workflow_intent_output_tooltips')}
id="intent-node-outputs"
name="outputs"
topLevelReadonly={true}
customReadonly
/>
);
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { nanoid } from 'nanoid';
import { ViewVariableType } from '@coze-workflow/variable';
import { INTENT_NODE_MODE } from '@coze-workflow/nodes';
import { I18n } from '@coze-arch/i18n';
// 入参路径,试运行等功能依赖该路径提取参数
export const INPUT_PATH = 'inputs.inputParameters';
export const INPUT_CHAT_HISTORY_SETTING_ENABLE =
'inputs.chatHistorySetting.enableChatHistory';
export const INPUT_CHAT_HISTORY_SETTING_ROUND =
'inputs.chatHistorySetting.chatHistoryRound';
export const INTENT_MODE = 'intentMode';
export const MODEL = 'model';
export const INTENTS = 'intents';
export const QUICK_INTENTS = 'quickIntents';
export const SYSTEM_PROMPT = 'systemPrompt';
export const COLUMNS = [
{
label: I18n.t('workflow_detail_node_parameter_name'),
style: { width: 148 },
},
{ label: I18n.t('workflow_detail_end_output_value') },
];
export const getDefaultOutputs = (intentMode: string) =>
[
{
key: nanoid(),
name: 'classificationId',
type: ViewVariableType.Integer,
},
intentMode === INTENT_NODE_MODE.STANDARD
? {
key: nanoid(),
name: 'reason',
type: ViewVariableType.String,
}
: '',
].filter(Boolean);

View File

@@ -0,0 +1,173 @@
/*
* 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, isNil, omit } from 'lodash-es';
import { type NodeFormContext } from '@flowgram-adapter/free-layout-editor';
import { variableUtils } from '@coze-workflow/variable';
import {
formatModelData,
getDefaultLLMParams,
INTENT_NODE_MODE,
} from '@coze-workflow/nodes';
import { type NodeDataDTO, type ValueExpression } from '@coze-workflow/base';
import { type FormData } from './types';
import { getDefaultOutputs } from './constants';
/**
* 节点后端数据 -> 前端表单数据
*/
export const transformOnInit = (
value: NodeDataDTO,
context: NodeFormContext,
) => {
const { playgroundContext } = context || {};
const { inputs, nodeMeta, outputs } = value || {};
let llmParam: Record<string, unknown> | undefined = get(
inputs,
'llmParam',
) as Record<string, unknown>;
const { models } = playgroundContext;
// 初次拖入画布时:从后端返回值里,解析出来默认值。
if (!llmParam) {
llmParam = getDefaultLLMParams(models);
}
const model: { [k: string]: unknown } = { ...llmParam };
const isNewCreateInInit = isNil(inputs);
const inputParameters = get(inputs, 'inputParameters', []);
// - If it is a new node, the default is fast mode, otherwise it is determined according to the backend return value (if there is no backend mode field, it means it is historical data, then it is standard mode)
// - The community version does not support the fast mode of intent recognition for future expansion
const intentModeInInit =
isNewCreateInInit && !IS_OPEN_SOURCE
? INTENT_NODE_MODE.MINIMAL
: (get(inputs, 'mode') as string) || INTENT_NODE_MODE.STANDARD;
const isMinimalMode = intentModeInInit === INTENT_NODE_MODE.MINIMAL;
const emptyIntent = [{ name: '' }];
const intentsValue = get(inputs, 'intents', emptyIntent);
return {
nodeMeta,
outputs: outputs || getDefaultOutputs(intentModeInInit),
model: omit(model, [
'enableChatHistory',
'systemPrompt',
'chatHistoryRound',
]) as { [k: string]: unknown },
// 开源版本只支持标准模式
intentMode: intentModeInInit,
intents: isMinimalMode ? emptyIntent : intentsValue,
quickIntents: isMinimalMode ? intentsValue : emptyIntent,
inputs: {
chatHistorySetting: {
enableChatHistory: llmParam.enableChatHistory || false,
chatHistoryRound: llmParam.chatHistoryRound || 3,
},
inputParameters:
inputParameters.length === 0 ? [{ name: 'query' }] : inputParameters,
},
systemPrompt:
(llmParam?.systemPrompt as Record<string, Record<string, unknown>>)?.value
?.content ?? '',
};
};
/**
* 前端表单数据 -> 节点后端数据
* @param value
* @returns
*/
export const transformOnSubmit = (
value: FormData,
context: NodeFormContext,
): NodeDataDTO => {
const { playgroundContext } = context || {};
const {
model,
inputs,
intents,
quickIntents,
intentMode,
systemPrompt,
nodeMeta,
outputs,
} = value || {};
const { chatHistorySetting, inputParameters } = inputs || {};
const { enableChatHistory, chatHistoryRound } = chatHistorySetting || {};
const { models, globalState, variableService, node } =
playgroundContext || {};
const { isChatflow } = globalState || {};
const modelMeta = models.find(m => m.model_type === model.modelType);
const promptItem = {
type: 'literal',
content: '{{query}}',
};
const systemPromptItem = {
type: 'literal',
content: intentMode === INTENT_NODE_MODE.MINIMAL ? '' : systemPrompt,
};
const formattedValue: Record<string, unknown> = {
nodeMeta,
outputs,
inputs: {
...(inputs || {}),
inputParameters,
llmParam: {
...formatModelData(model, modelMeta),
modelName: modelMeta?.name ?? '',
prompt: variableUtils.valueExpressionToDTO(
promptItem as ValueExpression,
variableService,
{
node,
},
),
systemPrompt: variableUtils.valueExpressionToDTO(
systemPromptItem as ValueExpression,
variableService,
{
node,
},
),
// 如果是工作流,提交时默认关闭历史记录,对话流则按用户实际值提交
enableChatHistory: isChatflow ? Boolean(enableChatHistory) : false,
// 历史记录轮数
chatHistoryRound,
},
intents: intentMode === INTENT_NODE_MODE.MINIMAL ? quickIntents : intents,
mode: intentMode,
},
};
return formattedValue as unknown as NodeDataDTO;
};

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cloneDeep } from 'lodash-es';
import {
type Effect,
FlowNodeFormData,
} from '@flowgram-adapter/free-layout-editor';
import { INTENT_NODE_MODE, DEFAULT_OUTPUTS_PATH } from '@coze-workflow/nodes';
import { WorkflowLinesService } from '@/services/workflow-line-service';
import { getDefaultOutputs } from '@/node-registries/intent/constants';
/** 表单创建后,重新赋值,需要等待个延时时间 */
const DELAY_TIME = 200;
const MAX_COUNT_IN_MINIMAL_MODE = 10;
const isEmptyIntents = (intents: { name: string }[]) =>
intents.every(intent => !intent.name);
export const handleIntentModeChange: Effect = props => {
const { value, context } = props;
const isMinimal = value === INTENT_NODE_MODE.MINIMAL;
if (!context?.node) {
return;
}
// formData 为格式化后的后端数据
const formData = context.node.getData(FlowNodeFormData);
const lineService =
context.node?.getService<WorkflowLinesService>(WorkflowLinesService);
const lines = lineService.getAllLines();
// 获取所有从该节点出发的连线
const linesFrom = lines.filter(line => line.from.id === context.node.id);
setTimeout(() => {
const outputsFormItem =
formData.formModel.getFormItemByPath(DEFAULT_OUTPUTS_PATH);
// 同步输出,标准模式和极简模式不一致
if (outputsFormItem) {
outputsFormItem.value = getDefaultOutputs(value);
}
const intentsFormItem = formData.formModel.getFormItemByPath('/intents');
const quickIntentsFormItem =
formData.formModel.getFormItemByPath('/quickIntents');
// 从标准模式切换到极简模式,如果原来设置的意图数量大于极简模式的最大值,需要截断
if (
quickIntentsFormItem &&
isMinimal &&
Array.isArray(quickIntentsFormItem.value)
) {
const quickIntentsLength = quickIntentsFormItem.value.length;
if (
quickIntentsLength <= 1 &&
isEmptyIntents(quickIntentsFormItem.value)
) {
// 极速模式,如果当前极速模式没有意图,则从完整模式的意图中截取
quickIntentsFormItem.value = cloneDeep(
intentsFormItem?.value.slice(0, MAX_COUNT_IN_MINIMAL_MODE),
);
// 重新连线
linesFrom.forEach(line => {
lineService.createLine({
from: line.from.id,
to: line.to?.id,
fromPort: line.info?.fromPort,
toPort: line.info?.toPort,
});
});
}
}
if (intentsFormItem && !isMinimal && Array.isArray(intentsFormItem.value)) {
const intentsLength = intentsFormItem.value.length;
if (intentsLength <= 1 && isEmptyIntents(intentsFormItem.value)) {
// 完整模式,如果当前完整模式没有意图,则从极简模式的意图中截取
intentsFormItem.value = cloneDeep(quickIntentsFormItem?.value);
// 重新连线
linesFrom.forEach(line => {
lineService.createLine({
from: line.from.id,
to: line.to?.id,
fromPort: line.info?.fromPort,
toPort: line.info?.toPort,
});
});
}
}
// 触发一次校验
formData.formModel.validate();
}, DELAY_TIME);
};

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { get } from 'lodash-es';
import {
ValidateTrigger,
type FormMetaV2,
DataEvent,
} from '@flowgram-adapter/free-layout-editor';
import { INTENT_NODE_MODE } from '@coze-workflow/nodes';
import { nodeMetaValidate } from '@/nodes-v2/materials/node-meta-validate';
import { validateIntentsName } from '@/node-registries/intent/validator';
import { createValueExpressionInputValidate } from '@/node-registries/common/validators';
import {
fireNodeTitleChange,
provideNodeOutputVariablesEffect,
} from '@/node-registries/common/effects';
import { type FormData } from './types';
import { FormRender } from './form';
import { handleIntentModeChange } from './effects/intent-mode-effect';
import { transformOnInit, transformOnSubmit } from './data-transformer';
import { INTENT_MODE, INTENTS, QUICK_INTENTS } from './constants';
export const INTENT_FORM_META: FormMetaV2<FormData> = {
// 节点表单渲染
render: () => <FormRender />,
// 验证触发时机
validateTrigger: ValidateTrigger.onChange,
// 验证规则
validate: {
nodeMeta: nodeMetaValidate,
// 必填
'inputs.inputParameters.0.input': createValueExpressionInputValidate({
required: true,
}),
[`${INTENTS}.*`]: ({ value, formValues, name }) => {
if (get(formValues, INTENT_MODE) === INTENT_NODE_MODE.STANDARD) {
return validateIntentsName(value, get(formValues, INTENTS), name);
}
return undefined;
},
[`${QUICK_INTENTS}.*`]: ({ value, formValues, name }) => {
if (get(formValues, INTENT_MODE) === INTENT_NODE_MODE.MINIMAL) {
return validateIntentsName(value, get(formValues, QUICK_INTENTS), name);
}
return undefined;
},
},
// 副作用管理
effect: {
nodeMeta: fireNodeTitleChange,
outputs: provideNodeOutputVariablesEffect,
[INTENT_MODE]: [
{
effect: handleIntentModeChange,
event: DataEvent.onValueChange,
},
],
},
// 节点后端数据 -> 前端表单数据
formatOnInit: transformOnInit,
// 前端表单数据 -> 节点后端数据
formatOnSubmit: transformOnSubmit,
};

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.
*/
import { NodeConfigForm } from '@/node-registries/common/components';
import Outputs from './components/outputs';
import ModelSelect from './components/model-select';
import ModeRadio from './components/mode-radio';
import { Intents, QuickIntents } from './components/intents';
import InputsParameters from './components/inputs-parameters';
import AdvancedSetting from './components/advanced-setting';
export const FormRender = () => (
<NodeConfigForm>
<ModelSelect />
<ModeRadio />
<InputsParameters />
<Intents />
<QuickIntents />
<AdvancedSetting />
<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 { INTENT_NODE_REGISTRY } from './node-registry';
export { IntentContent } from './node-content';

View File

@@ -0,0 +1,29 @@
/*
* 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 { Intents } from '@/components/node-render/node-render-new/fields';
import { InputParameters, Outputs } from '../common/components';
export function IntentContent() {
return (
<>
<InputParameters />
<Outputs />
<Intents />
</>
);
}

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 { INTENT_FORM_META } from './form-meta';
import { INPUT_PATH } from './constants';
export const INTENT_NODE_REGISTRY: WorkflowNodeRegistry<NodeTestMeta> = {
type: StandardNodeType.Intent,
meta: {
nodeDTOType: StandardNodeType.Intent,
size: { width: DEFAULT_NODE_SIZE.width, height: 156.7 },
nodeMetaPath: DEFAULT_NODE_META_PATH,
outputsPath: DEFAULT_OUTPUTS_PATH,
inputParametersPath: INPUT_PATH, // 入参路径,试运行等功能依赖该路径提取参数
useDynamicPort: true,
getLLMModelIdsByNodeJSON: nodeJSON =>
nodeJSON?.data?.inputs?.llmParam?.modelType,
defaultPorts: [{ type: 'input' }],
test,
helpLink: '/open/docs/guides/intent_recognition_node',
},
formMeta: INTENT_FORM_META,
};

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 { 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, isChatflow } = context;
/** 不在会话流LLM 节点无需关联环境 */
const formData = node
.getData(FlowNodeFormData)
.formModel.getFormItemValueByPath('/');
const enableChatHistory =
formData?.inputs?.chatHistorySetting?.enableChatHistory;
if (!isChatflow || !enableChatHistory) {
return {};
}
return generateEnvToRelatedContextProperties({
isNeedBot: !isInProject,
isNeedConversation: true,
});
},
generateFormInputProperties(node) {
const formData = node
.getData(FlowNodeFormData)
.formModel.getFormItemValueByPath('/');
const parameters = formData?.inputs?.inputParameters;
return generateParametersToProperties(parameters, { node });
},
};

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 {
type InputValueVO,
type NodeDataDTO,
type OutputValueVO,
} from '@coze-workflow/base';
export type IntentsType = { name?: string; id?: string }[];
export interface FormData {
inputs: {
inputParameters: InputValueVO[];
chatHistorySetting: {
enableChatHistory?: boolean;
chatHistoryRound?: number;
};
};
model: { [k: string]: unknown };
intents: IntentsType;
quickIntents: IntentsType;
intentMode: string;
systemPrompt: string;
nodeMeta: NodeDataDTO['nodeMeta'];
outputs: OutputValueVO[];
}

View File

@@ -0,0 +1,52 @@
/*
* 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, isNil } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
const MAX_LENGTH = 1000;
export const validateIntentsName = (
value?: { name?: string; id?: string },
intents?: { name?: string; id?: string }[],
name?: string,
) => {
const data = get(value, 'name');
const namePath = name?.split('.') || [];
const idx = namePath[namePath.length - 1];
const names = intents?.map(item => item?.name);
if (!isNil(idx)) {
names?.splice(Number(idx), 1);
}
if (!data || data.trim() === '') {
return I18n.t('workflow_intent_matchlist_error1');
}
if (data.length > MAX_LENGTH) {
return I18n.t('workflow_intent_matchlist_error2');
}
if (names && names.includes(name)) {
return I18n.t(
'workflow_ques_ans_testrun_dulpicate',
{},
'选项内容不可重复',
);
}
return undefined;
};