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,10 @@
.line {
height: 1px;
margin-top: -3px;
margin-bottom: 14px;
background-color: #FFF;
}
.chat-history-text {
font-size: 12px;
}

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 React, { type FC } from 'react';
import { nanoid } from 'nanoid';
import { ViewVariableType, useNodeTestId } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { Switch, Tooltip } from '@coze-arch/coze-design';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import { type ComponentProps } from '@/nodes-v2/components/types';
import {
OutputTree,
type OutputTreeProps,
} from '@/form-extensions/components/output-tree';
import { FormCard } from '@/form-extensions/components/form-card';
import { ChatHistoryRound } from '@/components/chat-history-round';
import styles from './index.module.less';
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 interface ChatHistoryValue {
enableChatHistory: boolean;
chatHistoryRound: number;
}
export const ChatHistory: FC<
ComponentProps<ChatHistoryValue> & {
style: React.CSSProperties;
showLine?: boolean;
}
> = ({ value, onChange, name, style, showLine = true }) => {
const { getNodeSetterId } = useNodeTestId();
const readonly = useReadonly();
return (
<>
<FormCard.Action>
<Tooltip content={I18n.t('wf_chatflow_125')} position="right">
<div className="flex items-center gap-1" style={style}>
<div className={styles['chat-history-text']}>
{I18n.t('wf_chatflow_124')}
</div>
<Switch
size="mini"
checked={value.enableChatHistory}
data-testid={getNodeSetterId(name)}
onChange={checked => {
if (value.enableChatHistory === checked) {
return;
}
onChange?.({
...value,
enableChatHistory: checked,
});
}}
disabled={readonly}
/>
</div>
</Tooltip>
</FormCard.Action>
{value.enableChatHistory ? (
<div className="relative">
<OutputTree
id="chat-history"
readonly
value={VALUE}
defaultCollapse
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange={() => {}}
withDescription={false}
withRequired={false}
noCard
/>
{showLine ? <div className={styles.line} /> : null}
<ChatHistoryRound
value={value.chatHistoryRound}
readonly={readonly}
onChange={w => {
onChange({
...value,
chatHistoryRound: Number(w),
});
}}
/>
</div>
) : null}
</>
);
};
export const chatHistory = {
key: 'ChatHistory',
component: ChatHistory,
};

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 const REASONING_CONTENT_NAME = 'reasoning_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.
*/
export { provideReasoningContentEffect } from './provide-reasoning-content';
export {
formatReasoningContentOnInit,
formatReasoningContentOnSubmit,
sortOutputs,
isSystemReasoningContent,
omitSystemReasoningContent,
} from './utils';
export { REASONING_CONTENT_NAME } from './constants';

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type Effect,
DataEvent,
FlowNodeFormData,
type FormModelV2,
} from '@flowgram-adapter/free-layout-editor';
import { type ViewVariableTreeNode } from '@coze-workflow/base';
import { WorkflowModelsService } from '@/services';
import { getOutputs } from './utils';
function createEffect(): Effect {
return ({ value, context: { node } }) => {
const modelType = value?.modelType;
const form = node
?.getData(FlowNodeFormData)
?.getFormModel<FormModelV2>()?.nativeFormModel;
if (!form || !modelType) {
return;
}
const outputs = form.getValueIn<ViewVariableTreeNode[] | undefined>(
'outputs',
);
const isBatch = form.getValueIn('batchMode') === 'batch';
const modelsService = node.getService(WorkflowModelsService);
form.setValueIn(
'outputs',
getOutputs({ modelType, outputs, isBatch, modelsService }),
);
};
}
export const provideReasoningContentEffect = [
{
effect: createEffect(),
event: DataEvent.onValueChange,
},
];

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 { nanoid } from '@flowgram-adapter/free-layout-editor';
import {
ViewVariableType,
type ViewVariableTreeNode,
} from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { type WorkflowModelsService } from '@/services';
import { REASONING_CONTENT_NAME } from './constants';
const readonlyTooltip = I18n.t(
'workflow_250217_02',
undefined,
'推理内容,支持输出思维链的模型特有',
);
interface ViewVariableTreeNodeWithReadonly extends ViewVariableTreeNode {
readonly?: boolean;
readonlyTooltip?: string;
}
const generateReasoningContent = () => ({
key: nanoid(),
name: REASONING_CONTENT_NAME,
type: ViewVariableType.String,
readonly: true,
readonlyTooltip,
});
const isReasoningContent = (node: ViewVariableTreeNodeWithReadonly) =>
node.name === REASONING_CONTENT_NAME;
export const isSystemReasoningContent = (
node: ViewVariableTreeNodeWithReadonly,
) => !!(isReasoningContent(node) && node.readonly);
const excludeReasoningContent = (nodes?: ViewVariableTreeNode[]) =>
(nodes ?? []).filter(node => !isSystemReasoningContent(node));
const includeReasoningContent = (nodes?: ViewVariableTreeNode[]) =>
(nodes ?? []).filter(node => isSystemReasoningContent(node));
const addReasoningContent = (
value?: ViewVariableTreeNode[],
isBatch?: boolean,
) => {
if (!value) {
return value;
}
if (isBatch) {
return [
{
...value[0],
children: [
...excludeReasoningContent(value[0]?.children),
generateReasoningContent(),
],
},
];
}
return [...excludeReasoningContent(value), generateReasoningContent()];
};
const removeReasoningContent = (
value?: ViewVariableTreeNode[],
isBatch?: boolean,
) => {
if (!value) {
return value;
}
if (isBatch) {
const [one, ...rest] = value;
return [
{
...one,
children: excludeReasoningContent(one?.children),
},
...rest,
];
} else {
return [...excludeReasoningContent(value)];
}
};
function findReasoningContent(
outputs: ViewVariableTreeNodeWithReadonly[] | undefined,
isBatch: boolean,
fn: (node) => boolean,
): ViewVariableTreeNodeWithReadonly | undefined {
if (!outputs) {
return undefined;
}
if (isBatch) {
return outputs[0]?.children?.find(node => fn(node));
}
return outputs.find(node => fn(node));
}
/**
* output 属性排序,保证 reasoning content 在最下面
*/
export const sortOutputs = (
value: ViewVariableTreeNode[] | undefined,
isBatch?: boolean,
) => {
if (!value) {
return value;
}
if (isBatch) {
const [one, ...rest] = value;
return [
{
...one,
children: [
...excludeReasoningContent(one?.children),
...includeReasoningContent(one?.children),
],
},
...rest,
];
}
return [...excludeReasoningContent(value), ...includeReasoningContent(value)];
};
/**
* 根据模型类型获取输出
* @param modelType
* @param outputs
* @param isBatch
* @returns
*/
export function getOutputs({
modelType,
outputs,
isBatch,
modelsService,
}: {
modelType: number | undefined;
outputs: ViewVariableTreeNode[] | undefined;
isBatch: boolean;
modelsService: WorkflowModelsService;
}) {
if (!modelType) {
return outputs;
}
if (modelsService.isCoTModel(modelType)) {
outputs = addReasoningContent(outputs, isBatch);
} else {
outputs = removeReasoningContent(outputs, isBatch);
}
return outputs;
}
/**
* 初始化时格式化推理内容为只读
* @param outputs
* @param isBatch
* @returns
*/
export function formatReasoningContentOnInit({
outputs,
isBatch,
modelType,
modelsService,
}: {
outputs: ViewVariableTreeNode[] | undefined;
isBatch: boolean;
modelType?: number;
modelsService: WorkflowModelsService;
}) {
if (!outputs) {
return outputs;
}
let newOutputs: ViewVariableTreeNode[] | undefined = outputs;
if (modelType && modelsService.isCoTModel(modelType)) {
// 后端返回的没有readonly字段需要前端处理, 取第一个类型是string的reasoning_content
const reasoningContent = findReasoningContent(
outputs,
isBatch,
item => isReasoningContent(item) && item.type === ViewVariableType.String,
);
if (reasoningContent) {
reasoningContent.readonly = true;
reasoningContent.readonlyTooltip = readonlyTooltip;
} else {
// 存量数据兼容,如果是推理模型,则添加推理内容字段
newOutputs = addReasoningContent(outputs, isBatch);
}
}
return newOutputs;
}
/**
* 提交时格式化推理内容移除readonly
* @param outputs
* @param isBatch
* @returns
*/
export function formatReasoningContentOnSubmit(
outputs: ViewVariableTreeNodeWithReadonly[] | undefined,
isBatch: boolean,
) {
if (!outputs) {
return outputs;
}
const reasoningContent = findReasoningContent(
outputs,
isBatch,
isSystemReasoningContent,
);
if (reasoningContent?.readonly) {
delete reasoningContent.readonly;
}
if (reasoningContent?.readonlyTooltip) {
delete reasoningContent.readonlyTooltip;
}
return outputs;
}
/**
* 去除 outputs 中的 为readonly的 reasoning_content
*/
export const omitSystemReasoningContent = (
value: ViewVariableTreeNodeWithReadonly[] | undefined,
isBatch?: boolean,
) => {
// 批量,去除 children 中的 为readonly的 reasoning_content
if (isBatch) {
return value?.map(v => ({
...v,
children: v?.children?.filter(c => !isSystemReasoningContent(c)),
}));
}
// 单次,去除 value 中的 为readonly的 reasoning_content
return value?.filter(v => !isSystemReasoningContent(v));
};

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useForm } from '@flowgram-adapter/free-layout-editor';
/**
* 获取模型type
*/
export function useModelType() {
const form = useForm();
const modelType = form.getValueIn('model')?.modelType;
return modelType;
}

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

View File

@@ -0,0 +1,535 @@
/*
* 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, set, omit, isEmpty } from 'lodash-es';
import {
Field,
ValidateTrigger,
FieldArray,
type FieldRenderProps,
type FieldArrayRenderProps,
type FormMetaV2,
type FormRenderProps,
nanoid,
type Validate,
} from '@flowgram-adapter/free-layout-editor';
import { PublicScopeProvider } from '@coze-workflow/variable';
import { nodeUtils, ViewVariableType } from '@coze-workflow/nodes';
import {
BlockInput,
concatTestId,
type InputValueDTO,
type RefExpression,
type ValueExpression,
ValueExpressionType,
} 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 { type IModelValue } from '@/typing';
import { WorkflowModelsService } from '@/services';
import { provideNodeOutputVariablesEffect } from '@/nodes-v2/materials/provide-node-output-variables';
import { createProvideNodeBatchVariables } from '@/nodes-v2/materials/provide-node-batch-variable';
import { fireNodeTitleChange } from '@/nodes-v2/materials/fire-node-title-change';
import { createValueExpressionInputValidate } from '@/nodes-v2/materials/create-value-expression-input-validate';
import { ChatHistory } from '@/nodes-v2/llm/chat-history';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import { ValueExpressionInput } from '@/nodes-v2/components/value-expression-input';
import { Outputs } from '@/nodes-v2/components/outputs';
import { createNodeInputNameValidate } from '@/nodes-v2/components/node-input-name/validate';
import { NodeInputName } from '@/nodes-v2/components/node-input-name';
import { FormItemFeedback } from '@/nodes-v2/components/form-item-feedback';
import { BatchMode } from '@/nodes-v2/components/batch-mode';
import { Batch } from '@/nodes-v2/components/batch/batch';
import { useGetWorkflowMode, useGlobalState } from '@/hooks';
import { FormCard } from '@/form-extensions/components/form-card';
import { ColumnsTitleWithAction } from '@/form-extensions/components/columns-title-with-action';
import { ModelSelect } from '@/components/model-select';
import { nodeMetaValidate } from '../materials/node-meta-validate';
import { SettingOnError } from '../components/setting-on-error';
import NodeMeta from '../components/node-meta';
import { Vision, isVisionInput } from './vision';
import {
llmOutputTreeMetaValidator,
llmInputNameValidator,
} from './validators';
import {
getDefaultLLMParams,
modelItemToBlockInput,
reviseLLMParamPair,
} from './utils';
import { UserPrompt } from './user-prompt';
import { type FormData } from './types';
import { SystemPrompt } from './system-prompt';
import { type BoundSkills } from './skills/types';
import {
formatFcParamOnInit,
formatFcParamOnSubmit,
} from './skills/data-transformer';
import { Skills } from './skills';
import {
formatReasoningContentOnInit,
formatReasoningContentOnSubmit,
provideReasoningContentEffect,
sortOutputs,
} from './cot';
import styles from './index.module.less';
/** 默认会话轮数 */
const DEFAULT_CHAT_ROUND = 3;
const Render = ({ form }: FormRenderProps<FormData>) => {
const readonly = useReadonly();
const { isChatflow } = useGetWorkflowMode();
const { isBindDouyin } = useGlobalState();
return (
<PublicScopeProvider>
<>
<NodeMeta
deps={['outputs', 'batchMode']}
outputsPath={'outputs'}
batchModePath={'batchMode'}
/>
<Field name={'batchMode'}>
{({ field }: FieldRenderProps<string>) => (
<BatchMode
name={field.name}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
/>
)}
</Field>
<Field name={'model'}>
{({ field }: FieldRenderProps<IModelValue | undefined>) => (
<FormCard
header={I18n.t('workflow_detail_llm_model')}
tooltip={I18n.t('workflow_detail_llm_prompt_tooltip')}
>
<ModelSelect {...field} readonly={readonly} />
</FormCard>
)}
</Field>
<Batch batchModeName={'batchMode'} name={'batch'} />
{!isBindDouyin ? (
<Field name="fcParam">
{({ field }: FieldRenderProps<BoundSkills | undefined>) => (
<Skills {...field} />
)}
</Field>
) : null}
<FieldArray
name={'$$input_decorator$$.inputParameters'}
defaultValue={[
{ name: 'input', input: { type: ValueExpressionType.REF } },
]}
>
{({
field,
}: FieldArrayRenderProps<{
name: string;
input: { type: ValueExpressionType };
}>) => (
<FormCard
header={I18n.t('workflow_detail_node_parameter_input')}
tooltip={I18n.t('workflow_detail_llm_input_tooltip')}
>
<div className={styles['columns-title']}>
<ColumnsTitleWithAction
columns={[
{
title: I18n.t('workflow_detail_variable_input_name'),
style: {
flex: 2,
},
},
{
title: I18n.t('workflow_detail_variable_input_value'),
style: {
flex: 3,
},
},
]}
readonly={readonly}
/>
</div>
{field.map((child, index) =>
isVisionInput(child.value) ? null : (
<div
key={child.key}
style={{
display: 'flex',
alignItems: 'flex-start',
paddingBottom: 4,
gap: 4,
}}
>
<Field name={`${child.name}.name`}>
{({
field: childNameField,
fieldState: nameFieldState,
}: FieldRenderProps<string>) => (
<div
style={{
flex: 2,
minWidth: 0,
}}
>
<NodeInputName
{...childNameField}
input={form.getValueIn<RefExpression>(
`${child.name}.input`,
)}
inputParameters={field.value || []}
isError={!!nameFieldState?.errors?.length}
/>
<FormItemFeedback errors={nameFieldState?.errors} />
</div>
)}
</Field>
<Field name={`${child.name}.input`}>
{({
field: childInputField,
fieldState: inputFieldState,
}: FieldRenderProps<ValueExpression | undefined>) => (
<div style={{ flex: 3, minWidth: 0 }}>
<ValueExpressionInput
{...childInputField}
isError={!!inputFieldState?.errors?.length}
/>
<FormItemFeedback errors={inputFieldState?.errors} />
</div>
)}
</Field>
{readonly ? (
<></>
) : (
<div className="leading-none">
<IconButton
size="small"
color="secondary"
data-testid={concatTestId(child.name, 'remove')}
icon={<IconCozMinus className="text-sm" />}
onClick={() => {
field.delete(index);
}}
/>
</div>
)}
</div>
),
)}
{isChatflow ? (
<Field name={'$$input_decorator$$.chatHistorySetting'}>
{({
field: enableChatHistoryField,
}: FieldRenderProps<{
enableChatHistory: boolean;
chatHistoryRound: number;
}>) => (
<ChatHistory
{...enableChatHistoryField}
style={{ paddingRight: '32px' }}
showLine={false}
/>
)}
</Field>
) : null}
{readonly ? (
<></>
) : (
<div className={styles['input-add-icon']}>
<IconButton
className="!block"
color="highlight"
size="small"
icon={<IconCozPlus className="text-sm" />}
onClick={() => {
field.append({
name: '',
input: { type: ValueExpressionType.REF },
});
}}
/>
</div>
)}
</FormCard>
)}
</FieldArray>
{!isBindDouyin ? <Vision /> : null}
<Field
name="$$prompt_decorator$$.systemPrompt"
deps={['$$input_decorator$$.inputParameters']}
defaultValue={''}
>
{({ field }: FieldRenderProps<string>) => (
<>
<SystemPrompt
{...field}
placeholder={I18n.t('workflow_detail_llm_sys_prompt_content')}
fcParam={form.getValueIn('fcParam')}
inputParameters={form.getValueIn(
'$$input_decorator$$.inputParameters',
)}
/>
</>
)}
</Field>
<Field
name="$$prompt_decorator$$.prompt"
deps={['$$input_decorator$$.inputParameters', 'model']}
defaultValue={''}
>
{({ field, fieldState }: FieldRenderProps<string>) => (
<>
<UserPrompt field={field} fieldState={fieldState} />
</>
)}
</Field>
<Field
name={'outputs'}
deps={['batchMode']}
defaultValue={[{ name: 'output', type: ViewVariableType.String }]}
>
{({ field, fieldState }) => (
<Outputs
id={'llm-node-output'}
value={field.value}
onChange={field.onChange}
batchMode={form.getValueIn('batchMode')}
withDescription
showResponseFormat
titleTooltip={I18n.t('workflow_detail_llm_output_tooltip')}
disabledTypes={[]}
needErrorBody={form.getValueIn(
'settingOnError.settingOnErrorIsOpen',
)}
errors={fieldState?.errors}
sortValue={sortOutputs}
/>
)}
</Field>
<SettingOnError outputsPath={'outputs'} batchModePath={'batchMode'} />
</>
</PublicScopeProvider>
);
};
const NEW_NODE_DEFAULT_VERSION = '3';
const userPromptFieldKey = '$$prompt_decorator$$.prompt';
export const LLM_FORM_META: FormMetaV2<FormData> = {
render: props => <Render {...props} />,
validateTrigger: ValidateTrigger.onChange,
validate: {
nodeMeta: nodeMetaValidate,
outputs: llmOutputTreeMetaValidator,
'$$input_decorator$$.inputParameters.*.name': llmInputNameValidator,
'$$input_decorator$$.inputParameters.*.input':
createValueExpressionInputValidate({ required: true }),
'batch.inputLists.*.name': createNodeInputNameValidate({
getNames: ({ formValues }) =>
(get(formValues, 'batch.inputLists') || []).map(item => item.name),
skipValidate: ({ formValues }) => formValues.batchMode === 'single',
}),
[userPromptFieldKey]: (({ value, formValues, context }) => {
const { playgroundContext } = context;
const modelType = get(formValues, 'model.modelType');
const curModel = playgroundContext?.models?.find(
model => model.model_type === modelType,
);
const isUserPromptRequired = curModel?.is_up_required ?? false;
if (!isUserPromptRequired) {
return undefined;
}
return value?.length
? undefined
: I18n.t('workflow_detail_llm_prompt_error_empty');
}) as Validate,
},
effect: {
nodeMeta: fireNodeTitleChange,
batchMode: createProvideNodeBatchVariables('batchMode', 'batch.inputLists'),
'batch.inputLists': createProvideNodeBatchVariables(
'batchMode',
'batch.inputLists',
),
outputs: provideNodeOutputVariablesEffect,
model: provideReasoningContentEffect,
},
// eslint-disable-next-line complexity
formatOnInit(value, context) {
const { node, playgroundContext } = context;
const modelsService = node.getService<WorkflowModelsService>(
WorkflowModelsService,
);
const models = modelsService.getModels();
let llmParam = get(value, 'inputs.llmParam');
// 初次拖入画布时:从后端返回值里,解析出来默认值。
if (!llmParam) {
llmParam = getDefaultLLMParams(models);
}
const model: { [k: string]: unknown } = {};
llmParam.forEach((d: InputValueDTO) => {
const [k, v] = reviseLLMParamPair(d);
model[k] = v;
});
const { prompt } = model;
delete model.prompt;
delete model.systemPrompt;
delete model.enableChatHistory;
const inputParameters = get(value, 'inputs.inputParameters');
const outputs = get(value, 'outputs');
const isBatch = get(value, 'inputs.batch.batchEnable');
const initValue = {
nodeMeta: value?.nodeMeta,
$$input_decorator$$: {
inputParameters: !inputParameters
? [{ name: 'input', input: { type: ValueExpressionType.REF } }]
: inputParameters,
chatHistorySetting: {
// 是否开启会话历史
enableChatHistory:
get(
llmParam.find(item => item.name === 'enableChatHistory'),
'input.value.content',
) || false,
// 会话轮数,默认为 3 轮
chatHistoryRound: Number(
get(
llmParam.find(item => item.name === 'chatHistoryRound'),
'input.value.content',
DEFAULT_CHAT_ROUND,
),
),
},
},
outputs: isEmpty(outputs)
? [{ name: 'output', type: ViewVariableType.String, key: nanoid() }]
: formatReasoningContentOnInit({
modelsService,
isBatch,
outputs,
modelType: model.modelType as number,
}),
// model 会根据 llmParam 重新填充值,此时也会将之前的 chatHistoryRound 也填充上去
// 由于在 submit 时会重新添加一个 chatHistoryRound这里先忽略掉避免出现问题
model: omit(model, ['chatHistoryRound']),
$$prompt_decorator$$: {
prompt,
systemPrompt: get(
llmParam.find(item => item.name === 'systemPrompt'),
'input.value.content',
),
},
batchMode: isBatch ? 'batch' : 'single',
batch: nodeUtils.batchToVO(get(value, 'inputs.batch'), context),
fcParam: formatFcParamOnInit(get(value, 'inputs.fcParam')),
};
// 获取后端下发 version 信息
const schema = JSON.parse(
playgroundContext.globalState.info?.schema_json || '{}',
);
const curNode = schema?.nodes?.find(_node => _node.id === node.id);
const versionFromBackend =
parseInt(curNode?.data?.version) >= parseInt(NEW_NODE_DEFAULT_VERSION)
? curNode?.data?.version
: NEW_NODE_DEFAULT_VERSION;
// 「LLM 节点订正需求 新增节点默认为 3」
set(initValue, 'version', versionFromBackend);
return initValue;
},
formatOnSubmit(value, context) {
const { node, playgroundContext } = context;
const { globalState } = playgroundContext;
const models = node
.getService<WorkflowModelsService>(WorkflowModelsService)
.getModels();
const { model } = value;
const modelMeta = models.find(m => m.model_type === model.modelType);
const llmParam = modelItemToBlockInput(model, modelMeta);
const { batchMode } = value;
const batchDTO = nodeUtils.batchToDTO(value.batch, context);
const prompt = BlockInput.createString(
'prompt',
value.$$prompt_decorator$$.prompt,
);
const enableChatHistory = BlockInput.createBoolean(
'enableChatHistory',
// 工作流没有会话历史,需要设置成 false会话流按照实际勾选的来
globalState.isChatflow
? Boolean(
get(
value,
'$$input_decorator$$.chatHistorySetting.enableChatHistory',
),
)
: false,
);
const chatHistoryRound = BlockInput.createInteger(
'chatHistoryRound',
get(value, '$$input_decorator$$.chatHistorySetting.chatHistoryRound'),
);
const systemPrompt = BlockInput.createString(
'systemPrompt',
get(value, '$$prompt_decorator$$.systemPrompt'),
);
llmParam.push(prompt, enableChatHistory, chatHistoryRound, systemPrompt);
const isBatch = batchMode === 'batch';
const formattedValue: Record<string, unknown> = {
nodeMeta: value.nodeMeta,
inputs: {
inputParameters: get(value, '$$input_decorator$$.inputParameters'),
llmParam,
fcParam: formatFcParamOnSubmit(value.fcParam),
batch: isBatch
? {
batchEnable: batchMode === 'batch',
...batchDTO,
}
: undefined,
},
outputs: formatReasoningContentOnSubmit(value.outputs, isBatch),
/**
* - 「LLM 节点 format优化」需求将 outputs 内容整合到 prompt 中限制输出格式,后端需要标志位区分逻辑,版本为 2
* - 「LLM 节点订正需求 兜底逻辑」,版本为 3
*/
version: NEW_NODE_DEFAULT_VERSION,
};
return formattedValue;
},
};

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.
*/
import {
DEFAULT_NODE_META_PATH,
type WorkflowNodeRegistry,
} from '@coze-workflow/nodes';
import { StandardNodeType } from '@coze-workflow/base';
import { type NodeTestMeta } from '@/test-run-kit';
import { test } from './node-test';
import { LLM_FORM_META } from './llm-form-meta';
export const LLM_NODE_REGISTRY: WorkflowNodeRegistry<NodeTestMeta> = {
type: StandardNodeType.LLM,
meta: {
nodeDTOType: StandardNodeType.LLM,
style: {
width: 360,
},
size: { width: 360, height: 130.7 },
test,
nodeMetaPath: DEFAULT_NODE_META_PATH,
batchPath: '/batch',
inputParametersPath: '/$$input_decorator$$/inputParameters',
getLLMModelIdsByNodeJSON: nodeJSON =>
nodeJSON.data.inputs.llmParam.find(p => p.name === 'modelType')?.input
.value.content,
helpLink: '/open/docs/guides/llm_node',
},
formMeta: LLM_FORM_META,
};

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 { 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?.$$input_decorator$$?.chatHistorySetting?.enableChatHistory;
if (!isChatflow || !enableChatHistory) {
return {};
}
return generateEnvToRelatedContextProperties({
isNeedBot: !isInProject,
isNeedConversation: true,
});
},
generateFormBatchProperties(node) {
const batchModePath = '/batchMode';
const batchDataPath = '/batch';
const { formModel } = node.getData(FlowNodeFormData);
const batchMode = formModel.getFormItemValueByPath(batchModePath);
if (batchMode !== 'batch') {
return {};
}
const batchData = formModel.getFormItemValueByPath(batchDataPath);
const parameters = batchData?.inputLists;
return generateParametersToProperties(parameters, { node });
},
generateFormInputProperties(node) {
const formData = node
.getData(FlowNodeFormData)
.formModel.getFormItemValueByPath('/');
const parameters = formData?.$$input_decorator$$?.inputParameters;
return generateParametersToProperties(parameters, { node });
},
};

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, expect, it } from 'vitest';
import {
formatFcParamOnInit,
formatFcParamOnSubmit,
} from '../data-transformer';
describe('data-transformer', () => {
it('formatFcParamOnInit with undefined', () => {
expect(formatFcParamOnInit(undefined)).toEqual(undefined);
});
it('formatFcParamOnInit with search_strategy', () => {
expect(
formatFcParamOnInit({
knowledgeFCParam: {
global_setting: {
search_strategy: 20,
min_score: 0.5,
top_k: 3,
auto: false,
show_source: false,
use_rerank: true,
use_rewrite: true,
use_nl2_sql: true,
},
},
}),
).toEqual({
knowledgeFCParam: {
global_setting: {
search_strategy: 20,
min_score: 0.5,
top_k: 3,
auto: false,
show_source: false,
use_rerank: true,
use_rewrite: true,
use_nl2_sql: true,
},
},
});
});
it('formatFcParamOnInit with search_mode', () => {
expect(
formatFcParamOnInit({
knowledgeFCParam: {
global_setting: {
search_mode: 20,
min_score: 0.5,
top_k: 3,
auto: false,
show_source: false,
use_rerank: true,
use_rewrite: true,
use_nl2_sql: true,
},
},
}),
).toEqual({
knowledgeFCParam: {
global_setting: {
search_strategy: 20,
min_score: 0.5,
top_k: 3,
auto: false,
show_source: false,
use_rerank: true,
use_rewrite: true,
use_nl2_sql: true,
},
},
});
});
it('formatFcParamOnSubmit with undefined', () => {
expect(formatFcParamOnSubmit(undefined)).toEqual(undefined);
});
it('formatFcParamOnSubmit with search_strategy', () => {
expect(
formatFcParamOnSubmit({
knowledgeFCParam: {
global_setting: {
search_strategy: 20,
min_score: 0.5,
top_k: 3,
auto: false,
show_source: false,
use_rerank: true,
use_rewrite: true,
use_nl2_sql: true,
},
},
}),
).toEqual({
knowledgeFCParam: {
global_setting: {
search_mode: 20,
min_score: 0.5,
top_k: 3,
auto: false,
show_source: false,
use_rerank: true,
use_rewrite: true,
use_nl2_sql: true,
},
},
});
});
});

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 { type FC, useState } from 'react';
import { AddIcon } from '@/nodes-v2/components/add-icon';
import { SkillModal, type SkillModalProps } from './skill-modal';
export const AddSkill: FC<
Omit<SkillModalProps, 'visible' | 'onCancel'> & {
disabledTooltip?: string;
}
> = props => {
const [modalVisible, setModalVisible] = useState(false);
const handleOpenModal = e => {
e.stopPropagation();
setModalVisible(true);
};
const handleCloseModal = () => setModalVisible(false);
return (
<div
onClick={e => {
e.stopPropagation();
}}
>
<AddIcon
disabledTooltip={props.disabledTooltip}
onClick={handleOpenModal}
/>
<SkillModal
visible={modalVisible}
onCancel={handleCloseModal}
{...props}
/>
</div>
);
};

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 FC } from 'react';
import { type WithCustomStyle } from '@coze-workflow/base/types';
import { type PluginFCSetting } from './types';
interface AsyncParamsFormProps {
initValue?: PluginFCSetting['response_style'];
onChange: (value: PluginFCSetting['response_style']) => void;
}
export const AsyncParamsForm: FC<
WithCustomStyle<AsyncParamsFormProps>
> = props => <div>Async</div>;

View File

@@ -0,0 +1,132 @@
/*
* 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, type ReactNode } from 'react';
import copy from 'copy-to-clipboard';
import { type APIParameter } from '@coze-workflow/base/api';
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { CustomError } from '@coze-arch/bot-error';
import { IconCozCopy, IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tag, Toast } from '@coze-arch/coze-design';
import { VariableTypeTag } from '@/form-extensions/components/variable-type-tag';
import { IconNameDescCard } from '@/form-extensions/components/icon-name-desc-card';
import { TooltipAction } from './tooltip-action';
import { TypeMap } from './constants';
interface BoundItemCardProps {
iconUrl?: string;
title: string;
pasteTitle?: string;
params?: Array<APIParameter>;
description?: ReactNode;
settingRender: ReactNode;
versionRender?: ReactNode;
onRemove?: () => void;
readonly?: boolean;
hideActions?: boolean;
className?: string;
}
export const BoundItemCard: FC<BoundItemCardProps> = props => {
const {
title,
pasteTitle,
iconUrl,
description,
onRemove,
settingRender,
versionRender,
params,
readonly,
hideActions,
className,
} = props;
const handleCopy = () => {
try {
const res = copy(pasteTitle ?? title);
if (!res) {
throw new CustomError(ReportEventNames.copy, 'empty content');
}
Toast.success({
content: I18n.t('copy_success'),
showClose: false,
id: 'plugin_copy_id',
});
} catch {
Toast.warning({
content: I18n.t('copy_failed'),
showClose: false,
});
}
};
const actions = (
<>
{params ? (
<TooltipAction
tooltip={params.map(param => (
<div className="mb-3">
<div className="flex items-center mb-2 gap-1">
{param.name}
{TypeMap.get(param.type as number) ? (
<VariableTypeTag size="xs">
{TypeMap.get(param.type as number)}
</VariableTypeTag>
) : null}
{param.is_required ? (
<Tag size="mini" color="yellow">
{I18n.t('required')}
</Tag>
) : null}
</div>
<div className="font-normal coz-fg-secondary leading-4">
{param.desc}
</div>
</div>
))}
icon={<IconCozInfoCircle />}
/>
) : null}
<TooltipAction
tooltip={I18n.t('bot_edit_page_plugin_copy_tool_name_tip')}
icon={<IconCozCopy />}
onClick={handleCopy}
/>
{settingRender}
</>
);
return (
<IconNameDescCard
icon={iconUrl}
name={title}
description={description as string}
onRemove={onRemove}
actions={hideActions ? [] : actions}
nameSuffix={versionRender}
readonly={readonly}
className={className}
descriptionTooltipPosition="bottom"
/>
);
};

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 { I18n } from '@coze-arch/i18n';
export const defaultKnowledgeGlobalSetting = {
auto: false,
min_score: 0.5,
no_recall_reply_customize_prompt: I18n.t('No_recall_006'),
no_recall_reply_mode: 0,
search_strategy: 0,
show_source: false,
show_source_mode: 0,
top_k: 3,
use_rerank: true,
use_rewrite: true,
use_nl2_sql: true,
};
export const defaultResponseStyleMode = 0;
// eslint-disable-next-line @typescript-eslint/naming-convention
export const TypeMap = new Map([
[1, 'String'],
[2, 'Integer'],
[3, 'Number'],
[4, 'Object'],
[5, 'Array'],
[6, 'Bool'],
]);

View File

@@ -0,0 +1,88 @@
/*
* 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, isUndefined, set } from 'lodash-es';
import {
type BoundKnowledgeItem,
type BoundSkills,
type KnowledgeGlobalSetting,
} from './types';
interface KnowledgeGlobalSettingDTO extends KnowledgeGlobalSetting {
search_mode?: number;
}
interface FunctionCallParamDTO extends BoundSkills {
knowledgeFCParam?: {
knowledgeList?: Array<BoundKnowledgeItem>;
global_setting?: KnowledgeGlobalSettingDTO;
};
}
type FunctionCallParamVO = BoundSkills;
/**
* fc参数后端转前端
* @param fcParamDTO
* @returns
*/
export function formatFcParamOnInit(fcParamDTO?: FunctionCallParamDTO) {
if (!fcParamDTO) {
return fcParamDTO;
}
const searchMode = get(
fcParamDTO,
'knowledgeFCParam.global_setting.search_mode',
);
if (isUndefined(searchMode)) {
return fcParamDTO;
}
delete fcParamDTO?.knowledgeFCParam?.global_setting?.search_mode;
set(
fcParamDTO,
'knowledgeFCParam.global_setting.search_strategy',
searchMode,
);
return fcParamDTO;
}
/**
* fc参数前端转后端
* @param fcParamVO
* @returns
*/
export function formatFcParamOnSubmit(fcParamVO?: FunctionCallParamVO) {
if (!fcParamVO) {
return fcParamVO;
}
const searchStrategy = get(
fcParamVO,
'knowledgeFCParam.global_setting.search_strategy',
);
if (isUndefined(searchStrategy)) {
return fcParamVO;
}
delete fcParamVO?.knowledgeFCParam?.global_setting?.search_strategy;
set(fcParamVO, 'knowledgeFCParam.global_setting.search_mode', searchStrategy);
return fcParamVO;
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozEmpty } from '@coze-arch/coze-design/icons';
import { EmptyState } from '@coze-arch/coze-design';
export function EmptySkill() {
return (
<div className="flex justify-center pt-[13px] pb-[3px]">
<EmptyState
icon={<IconCozEmpty />}
size="default"
title={I18n.t('wf_chatflow_155')}
></EmptyState>
</div>
);
}

View File

@@ -0,0 +1,4 @@
.bound-item-disabled {
cursor: not-allowed;
opacity: 0.5;
}

View File

@@ -0,0 +1,302 @@
/*
* 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 complexity */
import { type FC } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Tag, Tooltip } from '@coze-arch/coze-design';
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import { useGlobalState } from '@/hooks';
import { FormCard } from '@/form-extensions/components/form-card';
import {
SubWorkflowSkillVersion,
ApiSkillVersion,
} from '../../../components/reference-node-info';
import { WorkflowSetting } from './workflow-setting';
import { isSkillsEmpty, getSkillsQueryParams } from './utils';
import { useQuerySettingDetail } from './use-query-setting-detail';
import { useModelSkillDisabled } from './use-model-skill-disabled';
import {
SkillType,
type BoundSkills,
type BoundWorkflowItem,
type BoundPluginItem,
type BoundKnowledgeItem,
type PluginFCSetting,
type KnowledgeGlobalSetting,
} from './types';
import { PluginSetting } from './plugin-setting';
import { KnowledgeSetting } from './knowledge-setting';
import { EmptySkill } from './empty-skill';
import { defaultKnowledgeGlobalSetting } from './constants';
import { BoundItemCard } from './bound-item-card';
import { AddSkill } from './add-skill';
interface SkillsProps {
value?: BoundSkills;
onChange?: (data: BoundSkills) => void;
}
export const Skills: FC<SkillsProps> = props => {
const { value = {}, onChange } = props;
const isEmpty = isSkillsEmpty(value);
const globalState = useGlobalState();
const node = useCurrentEntity();
const readonly = useReadonly();
const modelSkillDisabled = useModelSkillDisabled();
const disabledTooltip = modelSkillDisabled
? I18n.t('workflow_250310_03', undefined, '该模型不支持绑定技能')
: '';
const { data: skillsDetail, refetch } = useQuerySettingDetail({
workflowId: globalState.workflowId,
spaceId: globalState.spaceId,
nodeId: node.id,
...getSkillsQueryParams(value),
});
const handleSkillsChange = (
type: SkillType,
data:
| Array<BoundWorkflowItem>
| Array<BoundPluginItem>
| Array<BoundKnowledgeItem>,
) => {
if (type === SkillType.Plugin) {
onChange?.({
...value,
pluginFCParam: {
pluginList: data as Array<BoundPluginItem>,
},
});
} else if (type === SkillType.Workflow) {
onChange?.({
...value,
workflowFCParam: {
workflowList: data as Array<BoundWorkflowItem>,
},
});
} else if (type === SkillType.Knowledge) {
onChange?.({
...value,
knowledgeFCParam: {
...value.knowledgeFCParam,
knowledgeList: data as Array<BoundKnowledgeItem>,
global_setting:
value.knowledgeFCParam?.global_setting ??
defaultKnowledgeGlobalSetting,
},
});
}
// 表单的onChange 值传递是异步,所以这里延迟下
setTimeout(() => {
refetch();
}, 10);
};
const handleRemovePlugin = (plugin: BoundPluginItem) => () => {
onChange?.({
...value,
pluginFCParam: {
pluginList: value?.pluginFCParam?.pluginList?.filter(
item => item.api_id !== plugin.api_id,
),
},
});
};
const handleRemoveWorkflow = (workflow: BoundWorkflowItem) => () => {
onChange?.({
...value,
workflowFCParam: {
workflowList: value?.workflowFCParam?.workflowList?.filter(
item => item.workflow_id !== workflow.workflow_id,
),
},
});
};
const handleRemoveKnowledge = (knowledge: BoundKnowledgeItem) => () => {
onChange?.({
...value,
knowledgeFCParam: {
knowledgeList: value?.knowledgeFCParam?.knowledgeList?.filter(
item => item.id !== knowledge.id,
),
},
});
};
const handlePluginItemSettingChange =
(plugin: BoundPluginItem) => (setting: PluginFCSetting | undefined) => {
onChange?.({
...value,
pluginFCParam: {
pluginList: value.pluginFCParam?.pluginList?.map(item => {
if (item.api_id === plugin.api_id) {
return {
...item,
fc_setting: setting,
};
} else {
return item;
}
}),
},
});
};
const handleWorkflowItemSettingChange =
(workflow: BoundWorkflowItem) => (setting: PluginFCSetting | undefined) => {
onChange?.({
...value,
workflowFCParam: {
workflowList: value.workflowFCParam?.workflowList?.map(item => {
if (item.workflow_id === workflow.workflow_id) {
return {
...item,
fc_setting: setting,
};
} else {
return item;
}
}),
},
});
};
const handleKnowledgeGlobalSettingChange = (
setting: KnowledgeGlobalSetting | undefined,
) => {
onChange?.({
...value,
knowledgeFCParam: {
...value?.knowledgeFCParam,
global_setting: setting,
},
});
};
return (
<FormCard
header={I18n.t('chatflow_agent_skill_name')}
actionButton={
readonly ? null : (
<AddSkill
boundSkills={value}
onSkillsChange={handleSkillsChange}
disabledTooltip={disabledTooltip}
/>
)
}
>
{isEmpty ? <EmptySkill /> : null}
{value.pluginFCParam?.pluginList?.map(item => {
const pluginDetail = skillsDetail?.plugin_detail_map?.[item.plugin_id];
const apiDetail = skillsDetail?.plugin_api_detail_map?.[item.api_id];
const title =
pluginDetail && apiDetail
? `${pluginDetail?.name} / ${apiDetail?.name}`
: '';
return (
<BoundItemCard
title={title}
pasteTitle={apiDetail?.name}
description={apiDetail?.description}
iconUrl={pluginDetail?.icon_url}
onRemove={handleRemovePlugin(item)}
params={apiDetail?.parameters ?? []}
readonly={readonly}
hideActions={modelSkillDisabled}
versionRender={
<div className="flex gap-1">
<ApiSkillVersion
versionTs={item.plugin_version}
versionName={pluginDetail?.version_name}
latestVersionName={pluginDetail?.latest_version_name}
latestVersionTs={pluginDetail?.latest_version_ts}
pluginId={item.plugin_id}
/>
{disabledTooltip ? (
<Tooltip content={disabledTooltip}>
<Tag size="mini" color="yellow">
{I18n.t('workflow_250310_02', undefined, '已失效')}
</Tag>
</Tooltip>
) : null}
</div>
}
settingRender={
<PluginSetting
{...item}
setting={item.fc_setting}
onChange={handlePluginItemSettingChange(item)}
/>
}
/>
);
})}
{value.workflowFCParam?.workflowList?.map(item => {
const detail = skillsDetail?.workflow_detail_map?.[item.workflow_id];
return (
<BoundItemCard
title={detail?.name ?? ''}
description={detail?.description}
iconUrl={detail?.icon_url}
onRemove={handleRemoveWorkflow(item)}
params={detail?.api_detail?.parameters ?? []}
versionRender={
<SubWorkflowSkillVersion
versionName={item.workflow_version}
latestVersionName={detail?.latest_version_name}
workflowId={item.workflow_id}
/>
}
settingRender={
<WorkflowSetting
{...item}
setting={item.fc_setting}
onChange={handleWorkflowItemSettingChange(item)}
/>
}
/>
);
})}
{value.knowledgeFCParam?.knowledgeList?.map(item => {
const detail = skillsDetail?.dataset_detail_map?.[item.id];
return (
<BoundItemCard
title={detail?.name ?? ''}
iconUrl={detail?.icon_url}
onRemove={handleRemoveKnowledge(item)}
settingRender={
<KnowledgeSetting
setting={value.knowledgeFCParam?.global_setting}
onChange={handleKnowledgeGlobalSettingChange}
/>
}
/>
);
})}
</FormCard>
);
};

View File

@@ -0,0 +1,145 @@
/*
* 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, type RefObject } from 'react';
import { type WithCustomStyle } from '@coze-workflow/base/types';
import { I18n } from '@coze-arch/i18n';
import { Tooltip } from '@coze-arch/bot-semi';
import { IconInfo } from '@coze-arch/bot-icons';
import { Input, Switch, Table, Tag } from '@coze-arch/coze-design';
import { VariableTypeTag } from '@/form-extensions/components/variable-type-tag';
import { useTableScroll } from './use-table-scroll';
import { type FCRequestParamsSetting, type PluginFCSetting } from './types';
import { TypeMap } from './constants';
interface InputParamsFormProps {
initValue?: PluginFCSetting['request_params'];
onChange: (value: PluginFCSetting['request_params']) => void;
}
const GAP = 55;
export const InputParamsForm: FC<
WithCustomStyle<InputParamsFormProps>
> = props => {
const { initValue = [], onChange } = props;
const { containerRef, scroll } = useTableScroll(GAP);
const handleDefaultValueChange = index => (value: string) => {
onChange?.(
initValue.map((item, _index) => {
if (_index === index) {
return {
...item,
local_default: value,
};
} else {
return item;
}
}),
);
};
const handleEnableChange = index => (checked: boolean) => {
onChange?.(
initValue.map((item, _index) => {
if (_index === index) {
return {
...item,
local_disable: !checked,
};
} else {
return item;
}
}),
);
};
return (
<div ref={containerRef as RefObject<HTMLDivElement>} className="h-full">
<Table
tableProps={{
dataSource: initValue,
scroll,
columns: [
{
title: I18n.t('Create_newtool_s3_table_name'),
dataIndex: 'name',
width: 300,
render: (text, record: FCRequestParamsSetting, index) => (
<div>
<div className="flex items-center gap-2">
<div>{record.name}</div>
<VariableTypeTag>
{TypeMap.get(record.type as number)}
</VariableTypeTag>
{record.is_required ? (
<Tag color="red" size="mini">
{I18n.t('required')}
</Tag>
) : null}
</div>
<div className="coz-fg-dim font-normal leading-4">
{record.desc}
</div>
</div>
),
},
{
title: I18n.t(
'plugin_edit_tool_default_value_config_item_default_value',
),
dataIndex: 'defaultValue',
width: 160,
render: (text, record: FCRequestParamsSetting, index) => (
<Input
size="small"
defaultValue={record.local_default}
onChange={handleDefaultValueChange(index)}
/>
),
},
{
title: () => (
<div>
{I18n.t('plugin_edit_tool_default_value_config_item_enable')}
<Tooltip
content={I18n.t(
'plugin_bot_ide_plugin_setting_modal_item_enable_tip',
)}
>
<IconInfo className="relative left-[2px] top-[2px]" />
</Tooltip>
</div>
),
dataIndex: 'enable',
render: (text, record: FCRequestParamsSetting, index) => (
<Switch
size="mini"
checked={record.local_disable === false ? true : false}
onChange={handleEnableChange(index)}
/>
),
},
],
}}
/>
</div>
);
};

View File

@@ -0,0 +1,10 @@
.rag-mode-config-wrapper {
> div {
max-width: none;
height: auto;
padding: 0;
background-color: unset;
border-radius: 0;
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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, useState } from 'react';
import { omit } from 'lodash-es';
import {
RagModeConfiguration,
type IDataSetInfo,
} from '@coze-data/knowledge-modal-base';
import { I18n } from '@coze-arch/i18n';
import { Button, Modal } from '@coze-arch/coze-design';
import { type KnowledgeGlobalSetting } from './types';
import { defaultKnowledgeGlobalSetting } from './constants';
import styles from './knowledge-setting-form-modal.module.less';
interface KnowledgeSettingFormModalProps {
visible: boolean;
setting?: KnowledgeGlobalSetting;
onSubmit?: (setting?: KnowledgeGlobalSetting) => void;
onCancel: () => void;
}
export const KnowledgeSettingFormModal: FC<
KnowledgeSettingFormModalProps
> = props => {
const { visible, onSubmit } = props;
const [setting, updateSetting] = useState<KnowledgeGlobalSetting | undefined>(
props.setting ?? defaultKnowledgeGlobalSetting,
);
const handleSubmit = () => {
onSubmit?.(setting);
};
const handleOnChange = (newSetting: IDataSetInfo) => {
updateSetting({
use_rerank: newSetting.recall_strategy?.use_rerank ?? true,
use_rewrite: newSetting.recall_strategy?.use_rewrite ?? true,
use_nl2_sql: newSetting.recall_strategy?.use_nl2sql ?? true,
...omit(newSetting, ['recall_strategy']),
});
};
const getDataSetInfo = (value: KnowledgeGlobalSetting): IDataSetInfo => ({
recall_strategy: {
use_rerank: value.use_rerank ?? true,
use_rewrite: value.use_rewrite ?? true,
use_nl2sql: value.use_nl2_sql ?? true,
},
...omit(value, ['use_rerank', 'use_rewrite', 'use_nl2_sql']),
});
return (
<Modal
size="large"
height={700}
visible={visible}
onCancel={props.onCancel}
title={I18n.t('dataset_settings_title')}
footer={
<div>
<Button color="hgltplus" onClick={handleSubmit}>
{I18n.t('Save')}
</Button>
</div>
}
>
<div className={styles['rag-mode-config-wrapper']}>
<RagModeConfiguration
dataSetInfo={getDataSetInfo(setting as KnowledgeGlobalSetting)}
showTitle={false}
onDataSetInfoChange={handleOnChange}
showAuto={false}
showSourceDisplay={false}
showNL2SQLConfig
/>
</div>
</Modal>
);
};

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 { type FC, useState } from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozSetting } from '@coze-arch/coze-design/icons';
import { type KnowledgeGlobalSetting } from './types';
import { TooltipAction } from './tooltip-action';
import { KnowledgeSettingFormModal } from './knowledge-setting-form-modal';
interface KnowledgeSettingProps {
setting?: KnowledgeGlobalSetting;
onChange?: (setting?: KnowledgeGlobalSetting) => void;
}
export const KnowledgeSetting: FC<KnowledgeSettingProps> = props => {
const { setting, onChange } = props;
const [visible, setVisible] = useState(false);
const handleSubmit = (newSetting?: KnowledgeGlobalSetting) => {
onChange?.(newSetting);
setVisible(false);
};
return (
<>
<TooltipAction
tooltip={I18n.t('plugin_bot_ide_plugin_setting_icon_tip')}
icon={<IconCozSetting />}
onClick={() => setVisible(true)}
/>
<KnowledgeSettingFormModal
visible={visible}
setting={setting}
onSubmit={handleSubmit}
onCancel={() => setVisible(false)}
/>
</>
);
};

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 { type FC } from 'react';
import { cloneDeep, set } from 'lodash-es';
import { type WithCustomStyle } from '@coze-workflow/base/types';
import { I18n } from '@coze-arch/i18n';
import { Tree, Tooltip } from '@coze-arch/bot-semi';
import { IconInfo } from '@coze-arch/bot-icons';
import { Form, Select, Switch } from '@coze-arch/coze-design';
import { VariableTypeTag } from '@/form-extensions/components/variable-type-tag';
import { type FCResponseParamsSetting, type PluginFCSetting } from './types';
import { TypeMap } from './constants';
export interface ResponseSettings {
response_params?: PluginFCSetting['response_params'];
response_style?: PluginFCSetting['response_style'];
}
interface OutputParamsFormProps {
initValue?: ResponseSettings;
onChange: (value: ResponseSettings) => void;
}
export const OutputParamsForm: FC<
WithCustomStyle<OutputParamsFormProps>
> = props => {
const { initValue, onChange } = props;
const handleResponseModeChange = value => {
onChange?.({
...initValue,
response_style: {
mode: value,
},
});
};
const handleParamEnableChange =
(indexPath: Array<number | string>, item: FCResponseParamsSetting) =>
(enable: boolean) => {
if (!initValue) {
return;
}
const newValue = cloneDeep(initValue);
set(newValue.response_params ?? [], indexPath.join('.'), {
...item,
local_disable: !enable,
});
onChange?.(newValue);
};
const normalizeTreeData = (
responseParams: PluginFCSetting['response_params'],
indexPath: Array<number | string> = [],
) =>
responseParams?.map((item, index) => {
const currentIndexPath = [...indexPath, index];
return {
label: (
<div className="flex items-center text-xs">
<div className="flex-1">
<div className="flex items-center">
<div className="font-medium">{item.name}</div>
<div>
<VariableTypeTag>
{TypeMap.get(item.type as number)}
</VariableTypeTag>
</div>
</div>
{item.desc ? <div className="coz-fg-dim">{item.desc}</div> : null}
</div>
<div
style={{
flex: '0 0 60px',
}}
>
<Switch
size="mini"
checked={item.local_disable === true ? false : true}
onChange={handleParamEnableChange(currentIndexPath, item)}
/>
</div>
</div>
),
value: item.name,
key: currentIndexPath.join('-'),
children: item.sub_parameters
? normalizeTreeData(item.sub_parameters, [
...currentIndexPath,
'sub_parameters',
])
: null,
};
});
return (
<div className="h-full flex flex-col">
<>
<div>
<Form.Label text={I18n.t('skillset_241115_01')} />
</div>
<Select
className="mb-4"
defaultValue={initValue?.response_style?.mode}
disabled
optionList={[
{
label: I18n.t('skillset_241115_02'),
value: 1,
},
{
label: I18n.t('skillset_241115_03'),
value: 0,
},
]}
onChange={handleResponseModeChange}
/>
</>
<>
<div>
<Form.Label text={I18n.t('workflow_detail_end_output')} />
</div>
<div className="text-xs text-left coz-fg-secondary coz-mg-hglt py-2 px-3 rounded-lg">
{I18n.t('plugin_bot_ide_plugin_setting_modal_item_enable_tip')}
</div>
<div className="flex text-xs coz-fg-dim font-medium mt-3">
<div className="flex-1 pl-[26px]">
{I18n.t('Create_newtool_s3_table_name')}
</div>
<div
style={{
flex: '0 0 60px',
}}
>
{I18n.t('plugin_edit_tool_default_value_config_item_enable')}
<Tooltip content={I18n.t('plugin_bot_ide_output_param_enable_tip')}>
<IconInfo className="relative left-[2px] top-[3px]" />
</Tooltip>
</div>
</div>
<div className="flex-1 overflow-y-auto">
<Tree
treeData={normalizeTreeData(initValue?.response_params)}
defaultExpandAll
/>
</div>
</>
</div>
);
};

View File

@@ -0,0 +1,49 @@
/* stylelint-disable declaration-no-important */
.plugin-setting-form-modal {
:global(.semi-modal-body-wrapper) {
height: calc(100% - 48px);
margin: 0 !important;
}
:global(.semi-modal-body) {
height: 100%;
> :global(div) {
max-height: none !important;
>:global(div) {
max-height: none !important;
}
}
}
:global(.semi-modal-content) {
gap: 0 !important;
padding: 0 !important;
}
:global(.semi-navigation-list-wrapper) {
padding-top: 0 !important;
}
:global(.semi-navigation-item-selected) {
background-color: var(--coz-mg-primary) !important;
}
:global(.semi-navigation-item-normal:hover) {
background-color: var(--coz-mg-primary) !important;
}
:global(.semi-modal-close) {
position: absolute;
top: 16px;
right: 16px;
}
}
.plugin-setting-nav {
width: 188px !important;
padding: 0;
background: transparent !important;
border: none !important;
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, useState } from 'react';
import classnames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import {
Button,
Modal,
Nav,
NavItem,
Typography,
} from '@coze-arch/coze-design';
import { type PluginFCSetting } from './types';
import { OutputParamsForm, type ResponseSettings } from './output-params-form';
import { InputParamsForm } from './input-params-form';
import styles from './plugin-setting-form-modal.module.less';
interface PluginSettingFormModalProps {
visible: boolean;
setting?: PluginFCSetting;
onSubmit?: (setting?: PluginFCSetting) => void;
onCancel: () => void;
}
enum SettingNavKey {
Input = 'input',
Output = 'output',
}
export const PluginSettingFormModal: FC<
PluginSettingFormModalProps
> = props => {
const { visible, setting: originSetting, onSubmit, onCancel } = props;
const [activeKey, setActiveKey] = useState(SettingNavKey.Input);
const [setting, updateSetting] = useState<PluginFCSetting | undefined>(
originSetting,
);
const handleParamsChange =
(key: keyof PluginFCSetting) =>
(settingParams: PluginFCSetting[keyof PluginFCSetting]) => {
updateSetting({
...setting,
[key]: settingParams,
});
};
const handleResponseParamsChange = (responseSettings: ResponseSettings) => {
updateSetting({
...setting,
...responseSettings,
});
};
const handleSubmit = () => {
onSubmit?.(setting);
};
return (
<Modal
className={styles['plugin-setting-form-modal']}
size="xl"
visible={visible}
onCancel={onCancel}
bodyStyle={{
padding: 0,
}}
height={700}
footer={
<div className="pt-0 flex">
<div
className="coz-bg-primary p-4"
style={{
flex: '0 0 220px',
}}
></div>
<div className="flex-1 pb-4 pr-4">
<Button color="hgltplus" onClick={handleSubmit}>
{I18n.t('Save')}
</Button>
</div>
</div>
}
>
<div className="flex h-full">
<div
className="coz-bg-primary p-4"
style={{
flex: '0 0 220px',
}}
>
<Typography.Title heading={4} className="!mb-4 px-2">
{I18n.t('basic_setting')}
</Typography.Title>
<Nav
className={styles['plugin-setting-nav']}
selectedKeys={[activeKey]}
onSelect={({ itemKey }) => setActiveKey(itemKey as SettingNavKey)}
>
<NavItem
itemKey={SettingNavKey.Input}
text={I18n.t('Create_newtool_s2_title')}
></NavItem>
<NavItem
itemKey={SettingNavKey.Output}
text={I18n.t('Create_newtool_s3_Outputparameters')}
></NavItem>
</Nav>
</div>
<div className="flex-1 px-4 pt-[50px] relative h-full overflow-y-hidden">
<div
className={classnames(
{
hidden: activeKey !== SettingNavKey.Input,
block: activeKey === SettingNavKey.Input,
},
'h-full',
)}
>
<InputParamsForm
initValue={setting?.request_params}
onChange={handleParamsChange('request_params')}
/>
</div>
<div
className={classnames(
{
hidden: activeKey !== SettingNavKey.Output,
block: activeKey === SettingNavKey.Output,
},
'h-full',
)}
>
<OutputParamsForm
initValue={{
response_params: setting?.response_params,
response_style: setting?.response_style,
}}
onChange={handleResponseParamsChange}
/>
</div>
</div>
</div>
</Modal>
);
};

View File

@@ -0,0 +1,101 @@
/*
* 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, useState } from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozSetting } from '@coze-arch/coze-design/icons';
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
import { useQueryLatestFCSettings } from './use-query-latest-fc-settings';
import { type BoundPluginItem, type PluginFCSetting } from './types';
import { TooltipAction } from './tooltip-action';
import { PluginSettingFormModal } from './plugin-setting-form-modal';
import { defaultResponseStyleMode } from './constants';
interface PluginSettingProps extends BoundPluginItem {
setting?: PluginFCSetting;
onChange?: (setting?: PluginFCSetting) => void;
}
export const PluginSetting: FC<PluginSettingProps> = props => {
const { setting, onChange } = props;
const node = useCurrentEntity();
const mutation = useQueryLatestFCSettings({
nodeId: node.id,
});
const [latestSetting, setLatestSetting] = useState<
PluginFCSetting | undefined
>(undefined);
const [visible, setVisible] = useState(false);
const handleEdit = () => {
mutation.mutate(
{
pluginFCSetting: setting
? {
plugin_id: props.plugin_id,
api_id: props.api_id,
api_name: props.api_name,
is_draft: props.is_draft,
plugin_version: props.plugin_version,
...setting,
}
: {
plugin_id: props.plugin_id,
api_id: props.api_id,
api_name: props.api_name,
request_params: [],
response_params: [],
response_style: {
mode: defaultResponseStyleMode,
},
is_draft: props.is_draft,
plugin_version: props.plugin_version,
},
},
{
onSuccess: res => {
setLatestSetting(res?.plugin_fc_setting);
setVisible(true);
},
},
);
};
const handleSubmit = (newSetting?: PluginFCSetting) => {
onChange?.(newSetting);
setVisible(false);
};
return (
<>
<TooltipAction
tooltip={I18n.t('plugin_bot_ide_plugin_setting_icon_tip')}
icon={<IconCozSetting />}
onClick={handleEdit}
/>
{visible ? (
<PluginSettingFormModal
visible={visible}
setting={latestSetting}
onSubmit={handleSubmit}
onCancel={() => setVisible(false)}
/>
) : null}
</>
);
};

View File

@@ -0,0 +1,86 @@
/*
* 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 classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozKnowledgeFill } from '@coze-arch/coze-design/icons';
export enum SkillKnowledgeSiderCategory {
Library = 'library',
Project = 'project',
}
interface Props {
projectId?: string;
category?: string;
setCategory?: (category: SkillKnowledgeSiderCategory) => void;
}
interface SiderCategoryProps {
label: string;
selected: boolean;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}
const SiderCategory = ({ label, onClick, selected }: SiderCategoryProps) => (
<div
onClick={onClick}
className={classNames([
'flex items-center gap-[8px] px-[12px]',
'px-[12px] py-[6px] rounded-[8px]',
'cursor-pointer',
'hover:text-[var(--light-usage-text-color-text-0,#1c1f23)]',
'hover:bg-[var(--light-usage-fill-color-fill-0,rgba(46,50,56,5%))]',
selected &&
'text-[var(--light-usage-text-color-text-0,#1c1d23)] bg-[var(--light-usage-fill-color-fill-0,rgba(46,47,56,5%))]',
])}
>
<IconCozKnowledgeFill />
{label}
</div>
);
export const SkillKnowledgeSider: React.FC<Props> = ({
projectId,
category = SkillKnowledgeSiderCategory.Library,
setCategory,
}) => (
<>
<SiderCategory
label={I18n.t('project_resource_modal_library_resources', {
resource: I18n.t('resource_type_knowledge'),
})}
onClick={() => {
setCategory?.(SkillKnowledgeSiderCategory.Library);
}}
selected={category === SkillKnowledgeSiderCategory.Library}
/>
{projectId ? (
<SiderCategory
label={I18n.t('project_resource_modal_project_resources', {
resource: I18n.t('resource_type_knowledge'),
})}
onClick={() => {
setCategory?.(SkillKnowledgeSiderCategory.Project);
}}
selected={category === SkillKnowledgeSiderCategory.Project}
/>
) : null}
</>
);

View File

@@ -0,0 +1,49 @@
.main {
display: flex;
flex-wrap: nowrap;
width: 100%;
height: 100%;
.sider {
overflow-y: auto;
display: flex;
flex-direction: column;
width: 218px;
padding: 12px;
background-color: #ebedf0;
}
.content {
overflow-y: hidden;
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
background: #f7f7fa;
.filter {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 8px 24px;
}
.content-inner {
overflow-y: hidden;
display: flex;
flex: 1;
}
}
:global(.tool-tag-list) {
overflow: inherit;
}
}
.data-sets-content {
padding: 16px 24px;
}

View File

@@ -0,0 +1,295 @@
/*
* 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, useRef, useState } from 'react';
import classNames from 'classnames';
import { ModalI18nKey } from '@coze-workflow/components/workflow-modal';
import {
WorkflowModalFrom,
useWorkflowModalParts,
} from '@coze-workflow/components';
import { KnowledgeListModalContent } from '@coze-data/knowledge-modal-adapter';
import { I18n } from '@coze-arch/i18n';
import { UITabsModal } from '@coze-arch/bot-semi';
import { type Dataset } from '@coze-arch/bot-api/knowledge';
import { From } from '@coze-agent-ide/plugin-shared';
import { usePluginModalParts } from '@coze-agent-ide/bot-plugin-export/agentSkillPluginModal/hooks';
import { useGlobalState, useSpaceId, useNodeVersionService } from '@/hooks';
import { isDraftByProjectId } from './utils';
import {
SkillType,
type BoundSkills,
type BoundWorkflowItem,
type BoundPluginItem,
type BoundKnowledgeItem,
} from './types';
import {
SkillKnowledgeSider,
SkillKnowledgeSiderCategory,
} from './skill-knowledge-sider';
import s from './skill-modal.module.less';
export interface SkillModalProps {
visible: boolean;
onSkillsChange: (
type: SkillType,
data:
| Array<BoundWorkflowItem>
| Array<BoundPluginItem>
| Array<BoundKnowledgeItem>,
) => void;
boundSkills?: BoundSkills;
onCancel: () => void;
}
export const SkillModal: FC<SkillModalProps> = props => {
const { visible, onSkillsChange, boundSkills, onCancel } = props;
const { projectId, getProjectApi, playgroundProps } = useGlobalState();
const windowRef = useRef<WindowProxy | null>();
const spaceID = useSpaceId();
const nodeVersionService = useNodeVersionService();
const [category, setCategory] = useState<SkillKnowledgeSiderCategory>(
SkillKnowledgeSiderCategory.Library,
);
// plugin 添加弹窗
const pluginModalFrom = projectId
? From.ProjectWorkflow
: From.WorkflowAddNode;
const getOnSkillsChange = (type: SkillType) => data =>
onSkillsChange(type, data);
const [activeKey, setActiveKey] = useState<SkillType>(SkillType.Plugin);
const pluginModalParts = usePluginModalParts({
pluginApiList: boundSkills?.pluginFCParam?.pluginList ?? [],
onPluginApiListChange: getOnSkillsChange(SkillType.Plugin),
from: pluginModalFrom,
projectId,
openModeCallback: async val => {
if (!val) {
return;
}
if (
!(await nodeVersionService.addApiCheck(val.plugin_id, val.version_ts))
) {
return;
}
onSkillsChange(SkillType.Plugin, [
...(boundSkills?.pluginFCParam?.pluginList ?? []),
{
plugin_id: val.plugin_id as string,
api_id: val.api_id as string,
api_name: val.name as string,
plugin_version: val.version_ts || '',
is_draft: isDraftByProjectId(val.project_id),
},
]);
},
});
const sourceTitle = I18n.t('workflow_241119_01');
const workflowModalParts = useWorkflowModalParts({
from: projectId
? WorkflowModalFrom.ProjectWorkflowAddNode
: WorkflowModalFrom.WorkflowAddNode,
projectId,
workFlowList: (boundSkills?.workflowFCParam?.workflowList ?? []).map(
item => ({
workflow_id: item.workflow_id,
plugin_id: item.plugin_id,
name: '',
desc: '',
parameters: [],
plugin_icon: '',
}),
),
onWorkFlowListChange: () => null,
onAdd: async (val, config) => {
if (!val) {
return;
}
if (
!(await nodeVersionService.addSubWorkflowCheck(
val.workflow_id,
val.version_name,
))
) {
return;
}
onSkillsChange(SkillType.Workflow, [
...(boundSkills?.workflowFCParam?.workflowList ?? []),
{
plugin_id: val.plugin_id,
workflow_id: val.workflow_id,
plugin_version: '',
workflow_version: val.version_name || '',
is_draft: isDraftByProjectId(val.project_id),
},
]);
},
onRemove: val => {
onSkillsChange(
SkillType.Workflow,
(boundSkills?.workflowFCParam?.workflowList ?? []).filter(
item => item.workflow_id !== val.workflow_id,
),
);
},
i18nMap: {
[ModalI18nKey.ListItemRemove]: {
key: 'scene_workflow_delete_workflow_button',
options: { source: sourceTitle },
},
[ModalI18nKey.ListItemRemoveConfirmTitle]: {
key: 'scene_workflow_delete_workflow_popup_title',
options: { source: sourceTitle },
},
[ModalI18nKey.ListItemRemoveConfirmDescription]: {
key: 'scene_workflow_delete_workflow_popup_subtitle',
options: { source: sourceTitle },
},
},
});
const handleKnowledgeListChange = (data: Dataset[]) => {
onSkillsChange(
SkillType.Knowledge,
data?.map(item => ({
id: item.dataset_id as string,
name: item.name as string,
})),
);
};
return (
<UITabsModal
visible={visible}
onCancel={onCancel}
tabs={{
tabsProps: {
lazyRender: true,
activeKey,
onChange: (key: string) => setActiveKey(key as SkillType),
},
tabPanes: [
{
tabPaneProps: {
tab: I18n.t('Tools'),
itemKey: SkillType.Plugin,
},
content: (
<div className={s.main}>
<div className={s.sider}>{pluginModalParts.sider}</div>
<div className={s.content}>
<div className={s.filter}>{pluginModalParts.filter}</div>
<div className={s['content-inner']}>
{pluginModalParts.content}
</div>
</div>
</div>
),
},
{
tabPaneProps: {
tab: I18n.t('Workflow'),
itemKey: SkillType.Workflow,
},
content: (
<div className={s.main}>
<div className={s.sider}>{workflowModalParts.sider}</div>
<div className={s.content}>
<div className={s.filter}>{workflowModalParts.filter}</div>
<div className={s['content-inner']}>
{workflowModalParts.content}
</div>
</div>
</div>
),
},
{
tabPaneProps: {
tab: I18n.t('Datasets'),
itemKey: SkillType.Knowledge,
},
content: (
<div className={s.main}>
{projectId ? (
<div className={s.sider}>
<SkillKnowledgeSider
projectId={projectId}
category={category}
setCategory={setCategory}
/>
</div>
) : null}
<div className={classNames(s.content, s['data-sets-content'])}>
<div className={s['content-inner']}>
<KnowledgeListModalContent
projectID={
category === SkillKnowledgeSiderCategory.Project
? projectId
: undefined
}
datasetList={(
boundSkills?.knowledgeFCParam?.knowledgeList ?? []
).map(item => ({
dataset_id: item.id,
name: item.name,
}))}
onDatasetListChange={handleKnowledgeListChange}
beforeCreate={shouldUpload => {
if (shouldUpload && !projectId) {
windowRef.current = window.open();
}
}}
onClickAddKnowledge={(id, unitType, shouldUpload) => {
if (shouldUpload) {
if (projectId) {
const IDENav = getProjectApi()?.navigate;
IDENav?.(
`/knowledge/${id}?module=upload&type=${unitType}`,
);
} else if (windowRef.current) {
if (id) {
windowRef.current.location = `/space/${spaceID}/knowledge/${id}/upload?type=${unitType}`;
} else {
windowRef.current.close();
}
}
}
if (projectId) {
playgroundProps.refetchProjectResourceList?.();
}
}}
/>
</div>
</div>
</div>
),
},
],
}}
/>
);
};

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 { TooltipAction } from '../../../form-extensions/components/icon-name-desc-card/';

View File

@@ -0,0 +1,85 @@
/*
* 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 IDataSetInfo } from '@coze-data/knowledge-modal-base';
import {
type FCPluginSetting,
type APIParameter,
type FCWorkflowSetting,
} from '@coze-arch/idl/workflow_api';
export enum SkillType {
Plugin = 'plugin',
Workflow = 'workflow',
Knowledge = 'knowledge',
}
export type PluginFCParamsSetting = APIParameter;
export type FCRequestParamsSetting = PluginFCParamsSetting;
export type FCResponseParamsSetting = PluginFCParamsSetting;
export interface FCResponseStyleSetting {
mode: number;
}
export type PluginFCSetting = FCPluginSetting;
export type WorkflowFCSetting = FCWorkflowSetting;
export interface BoundWorkflowItem {
plugin_id: string;
workflow_id: string;
// 如果是project 填project version资源库填plugin version
plugin_version: string;
workflow_version: string;
// 如果是project 就填true资源库 false
is_draft: boolean;
fc_setting?: WorkflowFCSetting;
}
export interface BoundPluginItem {
plugin_id: string;
api_id: string;
api_name: string;
// 如果是project 填project version资源库填plugin version
plugin_version: string;
// 如果是project 就填true资源库 false
is_draft: boolean;
fc_setting?: PluginFCSetting;
}
export interface BoundKnowledgeItem {
id: string;
name: string;
}
export type KnowledgeGlobalSetting = Omit<IDataSetInfo, 'recall_strategy'> & {
use_rerank: boolean;
use_rewrite: boolean;
use_nl2_sql: boolean;
};
export interface BoundSkills {
workflowFCParam?: {
workflowList?: Array<BoundWorkflowItem>;
};
pluginFCParam?: {
pluginList?: Array<BoundPluginItem>;
};
knowledgeFCParam?: {
knowledgeList?: Array<BoundKnowledgeItem>;
global_setting?: KnowledgeGlobalSetting;
};
}

View File

@@ -0,0 +1,31 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowModelsService } from '@/services';
import { useModelType } from '../hooks/use-model-type';
/**
* 判断模型是不是支持技能
*/
export function useModelSkillDisabled() {
const modelType = useModelType();
const modelsService = useService(WorkflowModelsService);
return !(modelType && modelsService.isFunctionCallModel(modelType));
}

View File

@@ -0,0 +1,169 @@
/*
* 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 { useMutation } from '@tanstack/react-query';
import { workflowApi } from '@coze-workflow/base/api';
import { useGlobalState } from '@/hooks';
import {
type WorkflowFCSetting,
type KnowledgeGlobalSetting,
type PluginFCSetting,
} from './types';
// const mockSettings = {
// plugin_fc_setting: {
// request_params: [
// {
// name: 'fieldName',
// type: 1, // String = 1, Integer = 2, Number = 3, Object = 4, Array = 5, Bool = 6,
// sub_type: 1,
// location: 1, // Path = 1, Query = 2, Body = 3, Header = 4,
// is_required: false,
// sub_parameters: [
// {
// name: 'fieldName',
// type: 1, // String = 1, Integer = 2, Number = 3, Object = 4, Array = 5, Bool = 6,
// sub_type: 1,
// location: 1, // Path = 1, Query = 2, Body = 3, Header = 4,
// is_required: false,
// local_default: '', // 默认值
// local_disable: false, // 是否启用
// assist_type: 1, //DEFAULT = 1, IMAGE = 2, DOC = 3,CODE = 4,PPT = 5, TXT = 6, EXCEL = 7, AUDIO = 8, ZIP = 9,VIDEO = 10,
// },
// ],
// local_default: '', // 默认值
// local_disable: false, // 是否启用
// assist_type: 1, //DEFAULT = 1, IMAGE = 2, DOC = 3,CODE = 4,PPT = 5, TXT = 6, EXCEL = 7, AUDIO = 8, ZIP = 9,VIDEO = 10,
// },
// ],
// response_params: [
// {
// name: 'fieldName',
// type: 1, // String = 1, Integer = 2, Number = 3, Object = 4, Array = 5, Bool = 6,
// sub_type: 1,
// location: 1, // Path = 1, Query = 2, Body = 3, Header = 4,
// is_required: false,
// sub_parameters: [
// {
// name: 'fieldName',
// type: 1, // String = 1, Integer = 2, Number = 3, Object = 4, Array = 5, Bool = 6,
// sub_type: 1,
// location: 1, // Path = 1, Query = 2, Body = 3, Header = 4,
// is_required: false,
// local_default: '', // 默认值
// local_disable: false, // 是否启用
// assist_type: 1, //DEFAULT = 1, IMAGE = 2, DOC = 3,CODE = 4,PPT = 5, TXT = 6, EXCEL = 7, AUDIO = 8, ZIP = 9,VIDEO = 10,
// },
// ],
// local_default: '', // 默认值
// local_disable: false, // 是否启用
// assist_type: 1, //DEFAULT = 1, IMAGE = 2, DOC = 3,CODE = 4,PPT = 5, TXT = 6, EXCEL = 7, AUDIO = 8, ZIP = 9,VIDEO = 10,
// },
// ],
// response_style: {
// mode: 1, // Raw = 0, // 原始输出 Card = 1, // 渲染成卡片 Template = 2, // 包含变量的模板内容用jinja2渲染 TODO
// },
// },
// workflow_fc_setting: {
// request_params: [
// {
// name: 'fieldName',
// type: 1, // String = 1, Integer = 2, Number = 3, Object = 4, Array = 5, Bool = 6,
// sub_type: 1,
// location: 1, // Path = 1, Query = 2, Body = 3, Header = 4,
// is_required: false,
// sub_parameters: [
// {
// name: 'fieldName',
// type: 1, // String = 1, Integer = 2, Number = 3, Object = 4, Array = 5, Bool = 6,
// sub_type: 1,
// location: 1, // Path = 1, Query = 2, Body = 3, Header = 4,
// is_required: false,
// local_default: '', // 默认值
// local_disable: false, // 是否启用
// assist_type: 1, //DEFAULT = 1, IMAGE = 2, DOC = 3,CODE = 4,PPT = 5, TXT = 6, EXCEL = 7, AUDIO = 8, ZIP = 9,VIDEO = 10,
// },
// ],
// local_default: '', // 默认值
// local_disable: false, // 是否启用
// assist_type: 1, //DEFAULT = 1, IMAGE = 2, DOC = 3,CODE = 4,PPT = 5, TXT = 6, EXCEL = 7, AUDIO = 8, ZIP = 9,VIDEO = 10,
// },
// ],
// response_params: [
// {
// name: 'fieldName',
// type: 1, // String = 1, Integer = 2, Number = 3, Object = 4, Array = 5, Bool = 6,
// sub_type: 1,
// location: 1, // Path = 1, Query = 2, Body = 3, Header = 4,
// is_required: false,
// sub_parameters: [
// {
// name: 'fieldName',
// type: 1, // String = 1, Integer = 2, Number = 3, Object = 4, Array = 5, Bool = 6,
// sub_type: 1,
// location: 1, // Path = 1, Query = 2, Body = 3, Header = 4,
// is_required: false,
// local_default: '', // 默认值
// local_disable: false, // 是否启用
// assist_type: 1, //DEFAULT = 1, IMAGE = 2, DOC = 3,CODE = 4,PPT = 5, TXT = 6, EXCEL = 7, AUDIO = 8, ZIP = 9,VIDEO = 10,
// },
// ],
// local_default: '', // 默认值
// local_disable: false, // 是否启用
// assist_type: 1, //DEFAULT = 1, IMAGE = 2, DOC = 3,CODE = 4,PPT = 5, TXT = 6, EXCEL = 7, AUDIO = 8, ZIP = 9,VIDEO = 10,
// },
// ],
// response_style: {
// mode: 1, // Raw = 0, // 原始输出 Card = 1, // 渲染成卡片 Template = 2, // 包含变量的模板内容用jinja2渲染 TODO
// },
// },
// dataset_fc_setting: {
// top_k: 5, // 召回数量
// min_score: 0.46, // 召回的最小相似度阈值
// auto: true, // 是否自动召回
// search_mode: 1, // 搜索策略
// no_recall_reply_mode: 1, // 无召回回复mode默认0
// no_recall_reply_customize_prompt:
// '抱歉,您的问题超出了我的知识范围,并且无法在当前阶段回答', // 无召回回复时自定义prompt当NoRecallReplyMode=1时生效
// show_source: true, // 是否展示来源
// show_source_mode: 1, // 来源展示方式 默认值0 卡片列表方式
// },
// }
export const useQueryLatestFCSettings = (params: { nodeId: string }) => {
const { workflowId, spaceId } = useGlobalState();
const mutation = useMutation({
mutationFn: (options: {
pluginFCSetting?: PluginFCSetting;
workflowFCSetting?: WorkflowFCSetting;
datasetFCSetting?: KnowledgeGlobalSetting;
}) =>
workflowApi.GetLLMNodeFCSettingsMerged({
workflow_id: workflowId,
space_id: spaceId,
plugin_fc_setting: options.pluginFCSetting,
workflow_fc_setting: options.workflowFCSetting,
}),
// return Promise.resolve({
// data: mockSettings,
// });
});
return mutation;
};

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 { useQuery, type UseQueryResult } from '@tanstack/react-query';
import {
type DatasetFCItem,
type GetLLMNodeFCSettingDetailResponse,
type PluginFCItem,
workflowApi,
type WorkflowFCItem,
} from '@coze-workflow/base/api';
import { PromiseLimiter } from '@/utils/promise-limiter';
// 限制并发因为同一个流程上可能会有很多LLM节点同时请求
const CONCURRENCY = 3;
const limiter = new PromiseLimiter(CONCURRENCY, true);
export const useQuerySettingDetail = (params: {
workflowId: string;
spaceId: string;
nodeId: string;
plugin_list?: Array<PluginFCItem>;
workflow_list?: Array<WorkflowFCItem>;
dataset_list?: Array<DatasetFCItem>;
enabled?: boolean;
}): UseQueryResult<GetLLMNodeFCSettingDetailResponse> => {
const { nodeId, enabled = true } = params;
return useQuery({
queryKey: [nodeId, 'settingDetail'],
queryFn: () =>
limiter.run(() =>
workflowApi.GetLLMNodeFCSettingDetail({
workflow_id: params.workflowId,
space_id: params.spaceId,
plugin_list: params.plugin_list,
workflow_list: params.workflow_list,
dataset_list: params.dataset_list,
}),
),
enabled,
});
};

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 { useMemo, useRef } from 'react';
import { useSize } from 'ahooks';
export function useTableScroll(gap: number) {
const containerRef = useRef<HTMLElement>(null);
const size = useSize(containerRef);
const scroll = useMemo(
() => ({ y: size?.height ? size.height - gap : 0 }),
[size, gap],
);
return {
containerRef,
scroll,
};
}

View File

@@ -0,0 +1,65 @@
/*
* 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 BoundSkills } from './types';
/**
* 根据projectId判断是否是草稿
* 资源库里面的插件 project_id = '0'
*/
export function isDraftByProjectId(projectId?: string) {
return projectId && projectId !== '0' ? true : false;
}
/**
* 技能是否为空
* @param value
* @returns
*/
export function isSkillsEmpty(value: BoundSkills) {
return (
!value.pluginFCParam?.pluginList?.length &&
!value.workflowFCParam?.workflowList?.length &&
!value.knowledgeFCParam?.knowledgeList?.length
);
}
/**
* 获取技能查询参数
* @param fcParam
* @returns
*/
export function getSkillsQueryParams(boundSkills?: BoundSkills) {
return {
plugin_list: boundSkills?.pluginFCParam?.pluginList?.map(item => ({
plugin_id: item.plugin_id,
api_id: item.api_id,
api_name: item.api_name,
is_draft: item.is_draft,
plugin_version: item.plugin_version,
})),
workflow_list: boundSkills?.workflowFCParam?.workflowList?.map(item => ({
workflow_id: item.workflow_id,
plugin_id: item.plugin_id,
is_draft: item.is_draft,
workflow_version: item.workflow_version,
})),
dataset_list: boundSkills?.knowledgeFCParam?.knowledgeList?.map(item => ({
dataset_id: item.id,
is_draft: false,
})),
};
}

View File

@@ -0,0 +1,98 @@
/*
* 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, useState } from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozSetting } from '@coze-arch/coze-design/icons';
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
import { useQueryLatestFCSettings } from './use-query-latest-fc-settings';
import { type BoundWorkflowItem, type PluginFCSetting } from './types';
import { TooltipAction } from './tooltip-action';
import { PluginSettingFormModal } from './plugin-setting-form-modal';
import { defaultResponseStyleMode } from './constants';
interface WorkflowSettingProps extends BoundWorkflowItem {
setting?: PluginFCSetting;
onChange?: (setting?: PluginFCSetting) => void;
}
export const WorkflowSetting: FC<WorkflowSettingProps> = props => {
const { setting, onChange } = props;
const node = useCurrentEntity();
const mutation = useQueryLatestFCSettings({
nodeId: node.id,
});
const [latestSetting, setLatestSetting] = useState<
PluginFCSetting | undefined
>(undefined);
const [visible, setVisible] = useState(false);
const handleEdit = () => {
mutation.mutate(
{
workflowFCSetting: setting
? {
workflow_id: props.workflow_id,
plugin_id: props.plugin_id,
is_draft: props.is_draft,
workflow_version: props.workflow_version,
...setting,
}
: {
workflow_id: props.workflow_id,
plugin_id: props.plugin_id,
request_params: [],
response_params: [],
workflow_version: props.workflow_version,
response_style: {
mode: defaultResponseStyleMode,
},
},
},
{
onSuccess: res => {
setLatestSetting(res?.worflow_fc_setting);
setVisible(true);
},
},
);
};
const handleSubmit = (newSetting?: PluginFCSetting) => {
onChange?.(newSetting);
setVisible(false);
};
return (
<>
<TooltipAction
tooltip={I18n.t('plugin_bot_ide_plugin_setting_icon_tip')}
icon={<IconCozSetting />}
onClick={handleEdit}
/>
{visible ? (
<PluginSettingFormModal
visible={visible}
setting={latestSetting}
onSubmit={handleSubmit}
onCancel={() => setVisible(false)}
/>
) : null}
</>
);
};

View File

@@ -0,0 +1,72 @@
/*
* 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, { useCallback } from 'react';
import { useForm, useRefresh } from '@flowgram-adapter/free-layout-editor';
import type { ILibraryItem } from '@coze-common/editor-plugins/library-insert';
import type { InputValueVO } from '@coze-workflow/base';
import { addSKillFromLibrary } from '@/nodes-v2/llm/utils';
import type { BoundSkills } from '../skills/types';
import {
SystemPrompt as DefaultSystemPrompt,
type SystemPromptProps,
} from '../../components/system-prompt';
import useSkillLibraries from './use-skill-libraries';
interface Props extends Omit<SystemPromptProps, 'libraries'> {
inputParameters?: InputValueVO[];
fcParam?: BoundSkills;
placeholder?: string;
}
export const SystemPrompt = (props: Props) => {
const { placeholder, inputParameters, fcParam, onAddLibrary, ...rest } =
props;
const form = useForm();
const refresh = useRefresh();
const { libraries, refetch } = useSkillLibraries({ fcParam });
const handleAddLibrary = useCallback(
(library: ILibraryItem) => {
form.setValueIn(
'fcParam',
addSKillFromLibrary(library, form.getValueIn('fcParam')),
);
refresh();
setTimeout(() => {
refetch();
}, 10);
},
[onAddLibrary],
);
return (
<DefaultSystemPrompt
{...rest}
onAddLibrary={handleAddLibrary}
libraries={libraries}
placeholder={placeholder}
inputParameters={inputParameters}
/>
);
};

View File

@@ -0,0 +1,228 @@
/*
* 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 {
type GetLLMNodeFCSettingDetailResponse,
WorkflowMode,
} from '@coze-arch/idl/workflow_api';
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
import {
type ILibraryList,
type ILibraryItem,
} from '@coze-common/editor-plugins/library-insert';
import { useGlobalState } from '@/hooks';
import { getSkillsQueryParams } from '../skills/utils';
import { useQuerySettingDetail } from '../skills/use-query-setting-detail';
import type { BoundKnowledgeItem, BoundSkills } from '../skills/types';
interface Props {
fcParam?: BoundSkills;
}
enum DatasetFormat {
Doc = 0,
Table = 1,
Image = 2,
Database = 3,
}
const formatSkills2Libraries = (
skillsDetail: GetLLMNodeFCSettingDetailResponse,
fcParam?: BoundSkills,
): ILibraryList => {
const result: ILibraryList = [];
const plugins: ILibraryItem[] | undefined =
fcParam?.pluginFCParam?.pluginList?.map(item => {
const pluginDetail = skillsDetail?.plugin_detail_map?.[item.plugin_id];
const apiDetail = skillsDetail?.plugin_api_detail_map?.[item.api_id];
return {
type: 'plugin',
id: item.plugin_id,
icon_url: pluginDetail?.icon_url || '',
name: apiDetail?.name || '',
desc: apiDetail?.description || '',
api_id: apiDetail?.id,
};
});
const workflows: ILibraryItem[] | undefined =
fcParam?.workflowFCParam?.workflowList
?.filter(item => {
const detail = skillsDetail?.workflow_detail_map?.[item.workflow_id];
return (
detail?.flow_mode === WorkflowMode.Workflow ||
detail?.flow_mode === WorkflowMode.ChatFlow
);
})
?.map(item => {
const detail = skillsDetail?.workflow_detail_map?.[item.workflow_id];
return {
type: 'workflow',
id: item.workflow_id,
icon_url: detail?.icon_url || '',
name: detail?.name || '',
desc: detail?.description || '',
};
});
const imageflows: ILibraryItem[] | undefined =
fcParam?.workflowFCParam?.workflowList
?.filter(item => {
const detail = skillsDetail?.workflow_detail_map?.[item.workflow_id];
return detail?.flow_mode === WorkflowMode.Imageflow;
})
?.map(item => {
const detail = skillsDetail?.workflow_detail_map?.[item.workflow_id];
return {
type: 'imageflow',
id: item.workflow_id,
icon_url: detail?.icon_url || '',
name: detail?.name || '',
desc: detail?.description || '',
};
});
const genDatasetFilter =
(target: DatasetFormat) => (item: BoundKnowledgeItem) => {
const detail = skillsDetail?.dataset_detail_map?.[item.id];
return detail?.format_type === target;
};
const tables: ILibraryItem[] | undefined =
fcParam?.knowledgeFCParam?.knowledgeList
?.filter(genDatasetFilter(DatasetFormat.Table))
?.map(item => {
const detail = skillsDetail?.dataset_detail_map?.[item.id];
return {
type: 'table',
id: item.id,
icon_url: detail?.icon_url || '',
name: detail?.name || '',
desc: '',
};
});
const images: ILibraryItem[] | undefined =
fcParam?.knowledgeFCParam?.knowledgeList
?.filter(genDatasetFilter(DatasetFormat.Image))
?.map(item => {
const detail = skillsDetail?.dataset_detail_map?.[item.id];
return {
type: 'image',
id: item.id,
icon_url: detail?.icon_url || '',
name: detail?.name || '',
desc: '',
};
});
const docs: ILibraryItem[] | undefined =
fcParam?.knowledgeFCParam?.knowledgeList
?.filter(genDatasetFilter(DatasetFormat.Doc))
?.map(item => {
const detail = skillsDetail?.dataset_detail_map?.[item.id];
return {
type: 'text',
id: item.id,
icon_url: detail?.icon_url || '',
name: detail?.name || '',
desc: '',
};
});
if (plugins?.length) {
result.push({
type: 'plugin',
items: plugins,
});
}
if (workflows?.length) {
result.push({
type: 'workflow',
items: workflows,
});
}
if (imageflows?.length) {
result.push({
type: 'imageflow',
items: imageflows,
});
}
if (tables?.length) {
result.push({
type: 'table',
items: tables,
});
}
if (images?.length) {
result.push({
type: 'image',
items: images,
});
}
if (docs?.length) {
result.push({
type: 'text',
items: docs,
});
}
return result;
};
export default function useSkillLibraries(props: Props) {
const { fcParam = {} } = props;
const globalState = useGlobalState();
const node = useCurrentEntity();
const { data: skillsDetail, refetch } = useQuerySettingDetail({
workflowId: globalState.workflowId,
spaceId: globalState.spaceId,
nodeId: node.id,
...getSkillsQueryParams(fcParam),
});
const libraries = useMemo(
() =>
formatSkills2Libraries(
skillsDetail as GetLLMNodeFCSettingDetailResponse,
fcParam,
),
[skillsDetail, fcParam],
);
return {
libraries,
refetch,
};
}

View File

@@ -0,0 +1,38 @@
/*
* 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 BatchVO, type InputValueVO } from '@coze-workflow/base';
import type { IModelValue } from '@/typing';
export enum BatchMode {
Single = 'single',
Batch = 'batch',
}
export interface FormData {
batchMode: BatchMode;
visionParam?: InputValueVO[];
model?: IModelValue;
$$input_decorator$$: {
inputParameters?: InputValueVO[];
chatHistorySetting?: {
enableChatHistory?: boolean;
chatHistoryRound?: number;
};
};
batch: BatchVO;
}

View File

@@ -0,0 +1,64 @@
/*
* 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, { useEffect } from 'react';
import { I18n } from '@coze-arch/i18n';
import { useForm } from '@flowgram-adapter/free-layout-editor';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import { FormItemFeedback } from '@/nodes-v2/components/form-item-feedback';
import { ExpressionEditor } from '@/nodes-v2/components/expression-editor';
import { useWorkflowModels } from '@/hooks';
import { FormCard } from '@/form-extensions/components/form-card';
import { CopyButton } from '@/components/copy-button';
export const UserPrompt = ({ field, fieldState }) => {
const form = useForm();
const readonly = useReadonly();
const { models } = useWorkflowModels();
const modelType = form.getValueIn('model.modelType');
const curModel = models?.find(model => model.model_type === modelType);
const isUserPromptRequired = curModel?.is_up_required ?? false;
useEffect(() => {
// TODO: 临时方案,待节点引擎提供新 api 后替换
field._fieldModel.validate();
}, [isUserPromptRequired]);
return (
<FormCard
key={'FormCard'}
header={I18n.t('workflow_detail_llm_prompt')}
tooltip={I18n.t('workflow_detail_llm_prompt_tooltip')}
required={isUserPromptRequired}
actionButton={
readonly ? [<CopyButton value={field?.value as string} />] : []
}
>
<ExpressionEditor
placeholder={I18n.t('workflow_detail_llm_prompt_content')}
maxLength={500}
{...field}
inputParameters={form.getValueIn('$$input_decorator$$.inputParameters')}
key="ExpressionEditor"
isError={!!fieldState?.errors?.length}
/>
<FormItemFeedback errors={fieldState?.errors} />
</FormCard>
);
};

View File

@@ -0,0 +1,197 @@
/*
* 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 { camelCase } from 'lodash-es';
import type {
ILibraryItem,
LibraryType,
} from '@coze-common/editor-plugins/library-insert';
import { DEFAULT_MODEL_TYPE } from '@coze-workflow/nodes';
import {
BlockInput,
GenerationDiversity,
VariableTypeDTO,
type InputValueDTO,
} from '@coze-workflow/base';
import { ModelParamType, type Model } from '@coze-arch/bot-api/developer_api';
import { isDraftByProjectId } from '@/nodes-v2/llm/skills/utils';
import {
type BoundKnowledgeItem,
type BoundPluginItem,
type BoundSkills,
type BoundWorkflowItem,
SkillType,
} from './skills/types';
import { defaultKnowledgeGlobalSetting } from './skills/constants';
const getDefaultModels = (modelMeta: Model): InputValueDTO[] => {
const defaultModel: InputValueDTO[] = [];
modelMeta?.model_params?.forEach(p => {
const k = camelCase(p.name) as string;
const { type } = p;
// 优先取平衡,自定义兜底
const defaultValue =
p.default_val[GenerationDiversity.Balance] ??
p.default_val[GenerationDiversity.Customize];
if (defaultValue !== undefined) {
if (ModelParamType.Float === type) {
defaultModel.push(BlockInput.createFloat(k, defaultValue));
} else if (ModelParamType.Int === type || ['modelType'].includes(k)) {
defaultModel.push(BlockInput.createInteger(k, defaultValue));
}
}
});
return defaultModel;
};
export const getDefaultLLMParams = (models: Model[]) => {
const modelMeta =
models.find(m => m.model_type === DEFAULT_MODEL_TYPE) ?? models[0];
const llmParam = [
BlockInput.createInteger('modelType', `${modelMeta?.model_type ?? ''}`),
BlockInput.createString('modelName', modelMeta?.name ?? ''),
BlockInput.createString('generationDiversity', GenerationDiversity.Balance),
...getDefaultModels(modelMeta),
].filter(Boolean);
return llmParam;
};
export const reviseLLMParamPair = (d: InputValueDTO): [string, unknown] => {
let k = d?.name || '';
// TODO 前端不依赖这个字段,确认后端无依赖后,可删除
// 兼容一个历史悠久的拼写错误
if (k === 'modleName') {
k = 'modelName';
}
let v = d.input.value.content;
if (
[VariableTypeDTO.float, VariableTypeDTO.integer].includes(
d.input.type as VariableTypeDTO,
)
) {
v = Number(d.input.value.content);
}
return [k, v];
};
export const modelItemToBlockInput = (
model: Model,
modelMeta: Model | undefined,
): BlockInput[] =>
Object.keys(model).map(k => {
const type = modelMeta?.model_params?.find(
p => camelCase(p.name) === k,
)?.type;
if (ModelParamType.Float === type) {
return BlockInput.createFloat(k, model[k]);
} else if (ModelParamType.Int === type || ['modelType'].includes(k)) {
return BlockInput.createInteger(k, model[k]);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
let _k = k;
// TODO 前端不依赖这个字段,确认后端无依赖后,可删除
if (_k === 'modelName') {
_k = 'modleName';
}
return BlockInput.createString(_k, model[k]);
});
const libraryType2SkillsType = (type: LibraryType): SkillType => {
if (type === 'plugin') {
return SkillType.Plugin;
}
if (type === 'imageflow' || type === 'workflow') {
return SkillType.Workflow;
}
return SkillType.Knowledge;
};
export const addSKillFromLibrary = (
library: ILibraryItem,
_skills: BoundSkills,
): BoundSkills => {
const type = libraryType2SkillsType(library.type);
const skills: BoundSkills = _skills || {};
if (type === SkillType.Plugin) {
const data = skills.pluginFCParam?.pluginList || [];
const detail = library.detail_info?.plugin_detail;
data.push({
plugin_id: detail?.plugin_id as string,
api_id: detail?.api_id as string,
api_name: detail?.name as string,
plugin_version: '', // 和 @张友松 确认 不传版本
is_draft: isDraftByProjectId(detail?.project_id),
});
return {
...skills,
pluginFCParam: {
pluginList: data as Array<BoundPluginItem>,
},
};
} else if (type === SkillType.Workflow) {
const data = skills.workflowFCParam?.workflowList || [];
const detail = library.detail_info?.workflow_detail;
data.push({
plugin_id: detail?.plugin_id as string,
workflow_id: detail?.workflow_id as string,
plugin_version: '',
workflow_version: '', // 和 @张友松 确认 不传版本
is_draft: isDraftByProjectId(detail?.project_id),
});
return {
...skills,
workflowFCParam: {
workflowList: data as Array<BoundWorkflowItem>,
},
};
} else if (type === SkillType.Knowledge) {
const data = skills.knowledgeFCParam?.knowledgeList || [];
const detail = library.detail_info?.knowledge_detail;
data.push({
id: detail?.id as string,
name: detail?.name as string,
});
return {
...skills,
knowledgeFCParam: {
...skills.knowledgeFCParam,
knowledgeList: data as Array<BoundKnowledgeItem>,
global_setting:
skills.knowledgeFCParam?.global_setting ??
defaultKnowledgeGlobalSetting,
},
};
}
return skills;
};

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 { llmOutputTreeMetaValidator } from './llm-output-tree-meta-validator';
export { llmInputNameValidator } from './llm-input-name-validator';

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 { get } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { nameValidationRule } from '@/nodes-v2/components/helpers';
import { isVisionEqual, isVisionInput } from '../vision';
export const llmInputNameValidator = ({ value, formValues, name }) => {
const validatorRule = nameValidationRule;
/** 命名校验 */
if (!validatorRule.test(value)) {
return I18n.t('workflow_detail_node_error_format');
}
const inputValues =
get(formValues, '$$input_decorator$$.inputParameters') || [];
const paths = name.split('.');
paths.pop();
const inputValue = get(formValues, paths);
if (!inputValue) {
return;
}
const sameVisionInputs = inputValues.filter(
item => item.name === value && isVisionEqual(item, inputValue),
);
// 都是输入或者视觉理解的场景直接返回重名
if (sameVisionInputs.length > 1) {
return I18n.t('workflow_detail_node_input_duplicated');
}
// 输入和视觉理解参数重名的场景,返回不能和视觉理解参数重名
// 视觉理解参数和输入重名,返回不能和输入重名
const differentVisionInputs = inputValues.filter(
item => item.name === value && !isVisionEqual(item, inputValue),
);
if (differentVisionInputs.length > 0) {
if (isVisionInput(inputValue)) {
return I18n.t('workflow_250317_01', undefined, '不能和输入重名');
} else {
return I18n.t('workflow_250317_02', undefined, '不能和视觉理解输入重名');
}
}
};

View File

@@ -0,0 +1,102 @@
/*
* 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';
import { omitSystemReasoningContent, REASONING_CONTENT_NAME } from '../cot';
/** 变量命名校验规则 */
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'),
)
.refine(
val => {
if (val === REASONING_CONTENT_NAME) {
return false;
}
return true;
},
{
message: I18n.t('workflow_250213_01'),
},
),
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 llmOutputTreeMetaValidator: Validate = ({ value, formValues }) => {
/**
* 判断错误异常处理是否打开,如果打开需要过滤掉 errorBody 后做校验
*/
const { settingOnErrorIsOpen = false } = (get(formValues, 'settingOnError') ??
{}) as { settingOnErrorIsOpen?: boolean };
/**
* 根据 batch 数据判断,当前是否处于批处理状态
*/
const batchValue = get(formValues, 'batchMode');
const isBatch = batchValue === 'batch';
const reasoningContentOmittedValue = omitSystemReasoningContent(
value,
isBatch,
);
const parsed = z
.array(OutputTreeMetaSchema)
.safeParse(
settingOnErrorIsOpen
? omitErrorBody(reasoningContentOmittedValue, isBatch)
: reasoningContentOmittedValue,
);
if (!parsed.success) {
const errorText = JSON.stringify((parsed as { error: ZodError }).error);
return errorText;
}
return undefined;
};

View File

@@ -0,0 +1,18 @@
/* stylelint-disable selector-class-pattern */
.vision-add-icon {
position: absolute;
top: 10px;
right: 12px;
}
.vision-title{
padding-bottom: 8px;
}
.vision-name-container {
:global {
.semi-input-wrapper__with-prefix {
padding-left: 0;
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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 FieldRenderProps,
type FieldArrayRenderProps,
} from '@flowgram-adapter/free-layout-editor';
import { type InputValueVO, type ViewVariableType } from '@coze-workflow/base';
import { IconCozMinus } from '@coze-arch/coze-design/icons';
import { IconButton } from '@coze-arch/coze-design';
import { isVisionInput } from '../utils/index';
import { VisionValueField } from './vision-value-field';
import { VisionNameField } from './vision-name-field';
interface VisionInputFieldProps {
inputField: FieldRenderProps<InputValueVO>['field'];
inputsField: FieldArrayRenderProps<InputValueVO>['field'];
index: number;
readonly?: boolean;
form;
enabledTypes: ViewVariableType[];
}
/**
* 输入字段
*/
export const VisionInputField: FC<VisionInputFieldProps> = ({
readonly,
inputField,
inputsField,
index,
enabledTypes,
}) => {
if (!isVisionInput(inputField?.value)) {
return null;
}
return (
<div className={'flex items-start pb-1 gap-1'}>
<VisionNameField
inputField={inputField}
inputsField={inputsField}
enabledTypes={enabledTypes}
/>
<VisionValueField
name={`${inputField.name}.input`}
enabledTypes={enabledTypes}
/>
{readonly ? (
<></>
) : (
<div className="leading-none">
<IconButton
size="small"
color="secondary"
icon={<IconCozMinus className="text-sm" />}
onClick={() => {
inputsField.delete(index);
}}
/>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,98 @@
/*
* 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 classNames from 'classnames';
import {
Field,
useForm,
type FieldArrayRenderProps,
type FieldRenderProps,
} from '@flowgram-adapter/free-layout-editor';
import { ViewVariableType, type InputValueVO } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tooltip } from '@coze-arch/coze-design';
import { NodeInputName } from '@/nodes-v2/components/node-input-name';
import { FormItemFeedback } from '@/nodes-v2/components/form-item-feedback';
import styles from './index.module.less';
interface VisionNameFieldProps {
inputField: FieldRenderProps<InputValueVO>['field'];
inputsField: FieldArrayRenderProps<InputValueVO>['field'];
enabledTypes: ViewVariableType[];
}
/**
* 输入名称字段
*/
export const VisionNameField: FC<VisionNameFieldProps> = ({
inputField,
inputsField,
enabledTypes,
}) => {
const form = useForm();
const input = form.getValueIn(`${inputField.name}.input`);
const disabledTooltip = useMemo(() => {
if (!enabledTypes.length) {
return I18n.t('workflow_250310_05', undefined, '所选模型不支持视觉理解');
}
const type = input?.rawMeta?.type;
if (!type || [...enabledTypes, ViewVariableType.String].includes(type)) {
return '';
}
return type === ViewVariableType.Image
? I18n.t('workflow_250320_01', undefined, '所选模型不支持图片理解')
: I18n.t('workflow_250320_02', undefined, '所选模型不支持视频理解');
}, [enabledTypes, input?.rawMeta?.type]);
return (
<Field name={`${inputField.name}.name`}>
{({
field: childNameField,
fieldState: nameFieldState,
}: FieldRenderProps<string>) => (
<div
className={classNames(
'flex-[2] min-w-0 relative',
styles['vision-name-container'],
)}
>
<NodeInputName
{...childNameField}
input={input}
inputParameters={inputsField.value || []}
isError={!!nameFieldState?.errors?.length}
inputPrefix={
disabledTooltip ? (
<Tooltip content={disabledTooltip}>
<IconCozInfoCircle className="coz-fg-hglt-yellow cursor-pointer text-sm" />
</Tooltip>
) : null
}
/>
<FormItemFeedback errors={nameFieldState?.errors} />
</div>
)}
</Field>
);
};

View File

@@ -0,0 +1,79 @@
/*
* 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 {
Field,
type FieldRenderProps,
} from '@flowgram-adapter/free-layout-editor';
import { type ValueExpression, ViewVariableType } from '@coze-workflow/base';
import { ValueExpressionInput } from '@/nodes-v2/components/value-expression-input';
import { FormItemFeedback } from '@/nodes-v2/components/form-item-feedback';
import { DEFUALT_VISION_INPUT } from '../constants';
interface VisionProps {
name: string;
enabledTypes: ViewVariableType[];
}
/**
* 输入值字段
* @returns */
export const VisionValueField: FC<VisionProps> = ({ enabledTypes, name }) => {
const disabledTypes = ViewVariableType.getComplement([
...enabledTypes,
ViewVariableType.String,
]);
return (
<Field name={name}>
{({
field: childInputField,
fieldState: inputFieldState,
}: FieldRenderProps<ValueExpression | undefined>) => (
<div className="flex-[3] min-w-0">
<ValueExpressionInput
{...childInputField}
isError={!!inputFieldState?.errors?.length}
disabledTypes={disabledTypes}
defaultInputType={enabledTypes[0]}
inputTypes={enabledTypes}
onChange={v => {
const expression = v as ValueExpression;
if (!expression) {
// 默认值需要带raw meta不然无法区分是不是视觉理解
childInputField?.onChange(DEFUALT_VISION_INPUT);
return;
}
const newExpression: ValueExpression = {
...expression,
rawMeta: {
...(expression.rawMeta || {}),
isVision: true,
},
};
childInputField?.onChange(newExpression);
}}
/>
<FormItemFeedback errors={inputFieldState?.errors} />
</div>
)}
</Field>
);
};

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 { type FC } from 'react';
import {
FieldArray,
useForm,
type FieldArrayRenderProps,
} from '@flowgram-adapter/free-layout-editor';
import { type InputValueVO, ValueExpressionType } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { useReadonly } from '@/nodes-v2/hooks/use-readonly';
import { AddIcon } from '@/nodes-v2/components/add-icon';
import { FormCard } from '@/form-extensions/components/form-card';
import { ColumnsTitleWithAction } from '@/form-extensions/components/columns-title-with-action';
import { useModelEnabledTypes } from '../hooks/use-model-enabled-types';
import { VisionInputField } from './vision-input-field';
import styles from './index.module.less';
interface VisionProps {
readonly?: boolean;
}
/**
* 视觉理解配置
*/
export const Vision: FC<VisionProps> = () => {
const enabledTypes = useModelEnabledTypes();
const disabledTooltip = !enabledTypes.length
? I18n.t('workflow_250310_05', undefined, '所选模型不支持视觉理解')
: '';
const form = useForm();
const readonly = useReadonly();
return (
<FieldArray name={'$$input_decorator$$.inputParameters'}>
{({ field }: FieldArrayRenderProps<InputValueVO>) => (
<FormCard
header={I18n.t('workflow_250310_04', undefined, '视觉理解输入')}
tooltip={I18n.t(
'workflow_250320_03',
undefined,
'用于视觉理解的输入传入图片or视频的url并在Prompt中应用该输入',
)}
>
<div className={styles['vision-title']}>
<ColumnsTitleWithAction
columns={[
{
title: I18n.t('workflow_detail_variable_input_name'),
style: {
flex: 2,
},
},
{
title: I18n.t('workflow_detail_variable_input_value'),
style: {
flex: 3,
},
},
]}
readonly={readonly}
/>
</div>
{field.map((child, index) => (
<VisionInputField
inputField={child}
inputsField={field}
index={index}
readonly={readonly}
form={form}
enabledTypes={enabledTypes}
key={child.key}
></VisionInputField>
))}
{readonly ? (
<></>
) : (
<div className={styles['vision-add-icon']}>
<AddIcon
disabledTooltip={disabledTooltip}
onClick={() => {
field.append({
name: '',
input: {
type: ValueExpressionType.REF,
rawMeta: { isVision: true },
},
});
}}
/>
</div>
)}
</FormCard>
)}
</FieldArray>
);
};

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.
*/
import { type ValueExpression, ValueExpressionType } from '@coze-workflow/base';
export const DEFUALT_VISION_INPUT: ValueExpression = {
type: ValueExpressionType.REF,
rawMeta: { isVision: true },
};

View File

@@ -0,0 +1,42 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { ViewVariableType } from '@coze-workflow/base';
import { WorkflowModelsService } from '@/services';
import { useModelType } from '../../hooks/use-model-type';
/**
* 模型支持的数据类型
*/
export function useModelEnabledTypes() {
const modelType = useModelType();
const modelsService = useService(WorkflowModelsService);
const modelAbility = modelsService.getModelAbility(modelType);
const enabledTypes: ViewVariableType[] = [];
if (modelAbility?.image_understanding) {
enabledTypes.push(ViewVariableType.Image);
}
if (modelAbility?.video_understanding) {
enabledTypes.push(ViewVariableType.Video);
}
return enabledTypes;
}

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 { Vision } from './components/vision';
export { isVisionInput, isVisionEqual } from './utils';

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 { isVisionInput } from './is-vision-input';
export { isVisionEqual } from './is-vision-equal';

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 InputValueVO } from '@coze-workflow/base';
import { isVisionInput } from './is-vision-input';
/**
* 判断是否是相同的输入类型
* @param value1
* @param value2
* @returns
*/
export const isVisionEqual = (
value1: InputValueVO,
value2: InputValueVO,
): boolean => isVisionInput(value1) === isVisionInput(value2);

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type InputValueVO } from '@coze-workflow/base';
/**
* 是不是视觉理解的输入
*/
export const isVisionInput = (value: InputValueVO): boolean =>
!!value?.input?.rawMeta?.isVision;