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,73 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type PropsWithChildren, useState } from 'react';
import cls from 'classnames';
import { IconCozArrowRight } from '@coze-arch/coze-design/icons';
import { Collapsible } from '@coze-arch/coze-design';
import { type VariableGroup } from '@/store';
export const GroupCollapsibleWrapper: FC<
PropsWithChildren<{
groupInfo: VariableGroup;
level?: number;
}>
> = props => {
const { groupInfo, children, level = 0 } = props;
const [isOpen, setIsOpen] = useState(true);
const isTopLevel = level === 0;
return (
<>
<div
className={cls(
'flex w-full flex-col cursor-pointer px-1 py-2',
isTopLevel ? 'hover:coz-mg-secondary-hovered hover:rounded-lg' : '',
)}
onClick={() => setIsOpen(!isOpen)}
>
<div className="flex items-center">
<div className="w-[22px] h-full flex items-center">
<IconCozArrowRight
className={cls('w-[14px] h-[14px]', isOpen ? 'rotate-90' : '')}
/>
</div>
<div className="w-[370px] h-full flex items-center">
<div
className={cls(
'coz-stroke-primary text-xxl font-medium',
!isTopLevel ? '!text-sm my-[10px]' : '',
)}
>
{groupInfo.groupName}
</div>
</div>
</div>
{isTopLevel ? (
<div className="text-sm coz-fg-secondary pl-[22px]">
{groupInfo.groupDesc}
</div>
) : null}
</div>
<Collapsible keepDOM isOpen={isOpen}>
<div className={cls('w-full h-full', !isTopLevel ? 'pl-[18px]' : '')}>
{children}
</div>
</Collapsible>
</>
);
};

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { VariableChannel } from '@coze-arch/bot-api/memory';
import { type VariableGroup } from '@/store';
import { flatGroupVariableMeta } from '../../../variable-tree/utils';
export const useGetHideKeys = (variableGroup: VariableGroup) => {
const hideKeys: string[] = [];
const hideChannel =
flatGroupVariableMeta([variableGroup]).filter(
item => (item?.effectiveChannelList?.length ?? 0) > 0,
).length <= 0;
const hideTypeChange = variableGroup.channel === VariableChannel.Custom;
if (hideChannel) {
hideKeys.push('channel');
}
if (hideTypeChange) {
hideKeys.push('type');
}
return hideKeys;
};

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 { VariableGroupParamHeader } from './render';
export { useGetHideKeys } from './hooks/use-get-hide-keys';

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
export const VariableGroupParamHeader = ({
hideHeaderKeys,
}: {
hideHeaderKeys?: string[];
}) => (
<div
className={cls(
'flex w-full h-[28px] py-[6px] pl-8 items-center gap-x-4 justify-start',
'border border-solid coz-stroke-primary border-t-0 border-x-0',
)}
>
<div className="flex-1 h-full flex items-center">
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
{I18n.t('bot_edit_memory_title_filed')}
<span className="coz-fg-hglt-red">*</span>
</div>
</div>
<div className="flex-1 h-full flex items-center">
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
{I18n.t('bot_edit_memory_title_description')}
</div>
</div>
{!hideHeaderKeys?.includes('type') ? (
<div className="flex-none w-[166px] basis-[166px] h-full flex items-center box-content">
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
{I18n.t('variable_Table_Title_type')}
</div>
</div>
) : null}
<div className="flex-none w-[164px] basis-[164px] h-full flex items-center box-content">
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
{I18n.t('bot_edit_memory_title_default')}
</div>
</div>
{!hideHeaderKeys?.includes('channel') ? (
<div className="flex-none w-[164px] basis-[164px] h-full flex items-center box-content">
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
{I18n.t('variable_Table_Title_support_channels')}
</div>
</div>
) : null}
<div className="flex-none w-[130px] basis-[130px] h-full flex items-center box-content">
<div className="coz-fg-secondary text-[12px] font-[500] leading-[16px]">
{I18n.t('bot_edit_memory_title_action')}
</div>
</div>
</div>
);

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 { type VariableGroup as VariableGroupType } from '@/store';
import { type TreeNodeCustomData } from '../variable-tree/type';
import { VariableTree } from '../variable-tree';
import { VariableGroupParamHeader, useGetHideKeys } from './group-header';
import { GroupCollapsibleWrapper } from './group-collapsible-wraper';
interface IVariableGroupProps {
groupInfo: VariableGroupType;
readonly?: boolean;
validateExistKeyword?: boolean;
onVariableChange: (changeValue: TreeNodeCustomData) => void;
}
export const VariableGroup = (props: IVariableGroupProps) => {
const {
groupInfo,
readonly = true,
validateExistKeyword = false,
onVariableChange,
} = props;
const hideHeaderKeys = useGetHideKeys(groupInfo);
return (
<>
<GroupCollapsibleWrapper groupInfo={groupInfo}>
<VariableGroupParamHeader hideHeaderKeys={hideHeaderKeys} />
<div className="pl-6">
{groupInfo.subGroupList?.map(subGroup => (
<GroupCollapsibleWrapper groupInfo={subGroup} level={1}>
<VariableTree
hideHeaderKeys={hideHeaderKeys}
groupId={groupInfo.groupId}
value={subGroup.varInfoList ?? []}
readonly={readonly}
channel={subGroup.channel}
validateExistKeyword={validateExistKeyword}
onChange={onVariableChange}
/>
</GroupCollapsibleWrapper>
))}
</div>
<div className="flex flex-col pl-6">
<VariableTree
hideHeaderKeys={hideHeaderKeys}
groupId={groupInfo.groupId}
value={groupInfo.varInfoList ?? []}
readonly={readonly}
channel={groupInfo.channel}
validateExistKeyword={validateExistKeyword}
onChange={onVariableChange}
/>
</div>
</GroupCollapsibleWrapper>
</>
);
};

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { IconAdd } from '@coze-arch/bot-icons';
import { IconCozAddNode } from '@coze-arch/coze-design/icons';
import { IconButton, type ButtonProps } from '@coze-arch/coze-design';
type AddOperationProps = React.PropsWithChildren<{
readonly?: boolean;
onClick: React.MouseEventHandler<HTMLButtonElement>;
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
subitem?: boolean;
size?: ButtonProps['size'];
color?: ButtonProps['color'];
}>;
export default function AddOperation({
readonly,
onClick,
className,
style,
disabled,
subitem = false,
size,
color,
...restProps
}: AddOperationProps) {
if (readonly) {
return null;
}
return (
<IconButton
data-testid={restProps['data-testid']}
onClick={onClick}
className={`${disabled ? 'disabled:text-[rgb(28,31,35,0.35)]' : 'text-[#4d53e8]'} ${className}`}
style={style}
icon={
subitem ? (
<IconCozAddNode />
) : (
<IconAdd className="text-[#4d53e8] disabled:text-[rgb(28,31,35,0.35)]" />
)
}
disabled={disabled}
size={size}
color={color}
/>
);
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type TreeNodeCustomData } from '@/components/variable-tree/type';
export const ParamChannel = (props: { value: TreeNodeCustomData }) => {
const { value } = props;
return value.effectiveChannelList?.length ? (
<div className="coz-stroke-primary text-[14px] font-[500] leading-[20px]">
{value.effectiveChannelList?.join(',') ?? '--'}
</div>
) : null;
};

View File

@@ -0,0 +1,141 @@
/*
* 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 { useState } from 'react';
import cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozEdit } from '@coze-arch/coze-design/icons';
import { CozInputNumber, Switch, Input } from '@coze-arch/coze-design';
import { ViewVariableType } from '@/store';
import { type TreeNodeCustomData } from '@/components/variable-tree/type';
import { ReadonlyText } from '../readonly-text';
import { JSONLikeTypes } from '../../constants';
import { JSONImport } from '../../../json-import';
interface ParamDefaultProps {
data: TreeNodeCustomData;
onDefaultChange: (value: string | number | boolean) => void;
onImportChange: (value: TreeNodeCustomData) => void;
readonly?: boolean;
}
export const ParamDefault = (props: ParamDefaultProps) => {
const { data, onDefaultChange, onImportChange, readonly } = props;
const [jsonModalVisible, setJsonModalVisible] = useState(false);
const isRoot = data.meta.level === 0;
const isString = isRoot && data.type === ViewVariableType.String;
const isNumber =
isRoot &&
(data.type === ViewVariableType.Number ||
data.type === ViewVariableType.Integer);
const isBoolean = isRoot && data.type === ViewVariableType.Boolean;
const isShowJsonImport = JSONLikeTypes.includes(data.type) && isRoot;
return (
<div className="w-[144px] h-full flex items-center [&_.semi-input-number-suffix-btns]:!h-auto">
<div className="flex flex-col w-full relative">
{readonly && !isShowJsonImport ? (
<ReadonlyText
className="w-[144px]"
value={data.defaultValue || '-'}
/>
) : (
<>
{isString ? (
<Input
value={data.defaultValue}
onChange={value => onDefaultChange(value)}
placeholder={I18n.t(
'workflow_detail_title_testrun_error_input',
{
a: data.name,
},
)}
maxLength={1000}
disabled={readonly}
/>
) : null}
{isNumber ? (
<CozInputNumber
className={cls('h-full', {
'[&_.semi-input-wrapper]:!coz-stroke-plus': true,
})}
value={data.defaultValue}
onChange={value => onDefaultChange(value)}
placeholder={I18n.t(
'workflow_detail_title_testrun_error_input',
{
a: data.name,
},
)}
disabled={readonly}
/>
) : null}
{isBoolean ? (
<Switch
checked={Boolean(data.defaultValue === 'true')}
size="small"
onChange={value => onDefaultChange(value)}
disabled={readonly}
/>
) : null}
{isShowJsonImport ? (
<>
<div
onClick={() => setJsonModalVisible(true)}
className={cls(
'coz-mg-primary rounded cursor-pointer flex items-center justify-center h-[32px] gap-x-1',
{
'coz-fg-primary': !readonly,
'coz-fg-dim': readonly,
},
)}
>
<IconCozEdit />
<span className="text-sm font-medium">
{readonly
? I18n.t('variables_json_input_readonly_button')
: I18n.t('variable_button_input_json')}
</span>
</div>
<JSONImport
visible={jsonModalVisible}
treeData={data}
rules={{
jsonImport: true,
readonly: Boolean(readonly),
}}
onOk={value => {
onImportChange(value);
setJsonModalVisible(false);
}}
onCancel={() => setJsonModalVisible(false)}
/>
</>
) : null}
</>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { Input } from '@coze-arch/coze-design';
import { type Variable } from '@/store';
import { ReadonlyText } from '../readonly-text';
export const ParamDescription = (props: {
data: Variable;
onChange: (value: string) => void;
readonly: boolean;
}) => {
const { data, onChange, readonly } = props;
return !readonly ? (
<div className="flex flex-col w-full relative overflow-hidden">
<Input
value={data.description}
placeholder={I18n.t('workflow_detail_llm_output_decription')}
maxLength={200}
onChange={value => {
onChange(value);
}}
className="w-full"
/>
</div>
) : (
<ReadonlyText value={data.description ?? ''} />
);
};

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useRef } from 'react';
import { useFormApi } from '@coze-arch/coze-design';
import { type Variable } from '@/store';
export const useCacheField = (data: Variable) => {
const formApi = useFormApi();
const lastValidValueRef = useRef(data.name);
useEffect(() => {
const currentValue = formApi.getValue(`${data.variableId}.name`);
if (currentValue) {
lastValidValueRef.current = currentValue;
} else if (lastValidValueRef.current) {
formApi.setValue(`${data.variableId}.name`, lastValidValueRef.current);
}
}, [data.variableId]);
};

View File

@@ -0,0 +1,106 @@
/*
* 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 cls from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { FormInput, useFormApi } from '@coze-arch/coze-design';
import { type Variable } from '@/store';
import { useVariableContext } from '@/context';
import { ReadonlyText } from '../readonly-text';
import {
requiredRules,
duplicateRules,
existKeywordRules,
} from './services/check-rules';
import { useCacheField } from './hooks/use-cache-field';
export const ParamName = (props: {
data: Variable;
readonly: boolean;
onChange: (value: string) => void;
validateExistKeyword?: boolean;
}) => {
const { data, onChange, readonly, validateExistKeyword = false } = props;
const { groups } = useVariableContext();
const formApi = useFormApi();
// 使用 ref 缓存最后一次的有效值, Tree组件隐藏的时候会销毁组件Form表单的Field字段会删除所以需要缓存
useCacheField(data);
return (
<div
className={cls(
'w-full overflow-hidden',
'[&_.semi-form-field-error-message]:absolute',
'[&_.semi-form-field-error-message]:text-[12px]',
'[&_.semi-form-field-error-message]:font-[400]',
'[&_.semi-form-field-error-message]:leading-[16px]',
)}
>
{!readonly ? (
<>
<FormInput
field={`${data.variableId}.name`}
placeholder={I18n.t('variable_name_placeholder')}
maxLength={50}
autoFocus={!data.name}
noLabel
rules={[
{
validator: (_, value) =>
requiredRules.validate({
...data,
name: value,
}),
message: requiredRules.message,
},
{
validator: (_, value) =>
validateExistKeyword
? existKeywordRules.validate({
...data,
name: value,
})
: true,
message: existKeywordRules.message,
},
{
validator: (_, value) =>
duplicateRules.validate(
{
...data,
name: value,
},
groups,
),
message: duplicateRules.message,
},
]}
onChange={value => {
onChange(value);
formApi.setValue(`${data.variableId}.name`, value);
}}
className="w-full truncate"
/>
</>
) : (
<ReadonlyText value={data.name} />
)}
</div>
);
};

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
import { VariableChannel } from '@coze-arch/bot-api/memory';
import { type Variable, type VariableGroup } from '@/store';
export const requiredRules = {
validate: (value: Variable) => !!value.name,
message: I18n.t('bot_edit_variable_field_required_error'),
};
/**
* 检查变量名称是否重复
* 1、检查变量名称在同组&同层级是否重复
* 2、检查变量名称在不同组的Root节点名称是否重复
*/
export const duplicateRules = {
validate: (value: Variable, groups: VariableGroup[]): boolean => {
if (!value.name) {
return true;
} // 如果名称为空则跳过检查
// 1. 检查同组同层级是否重复
const currentGroup = groups.find(group => group.groupId === value.groupId);
if (!currentGroup) {
return true;
}
// 获取当前节点所在的所有同层级节点包括嵌套在其他节点children中的
const findSiblings = (
variables: Variable[],
targetParentId: string | null,
): Variable[] => {
let result: Variable[] = [];
for (const variable of variables) {
// 如果当前变量的parentId与目标parentId相同且不是自身则添加到结果中
if (
variable.parentId === targetParentId &&
variable.variableId !== value.variableId
) {
result.push(variable);
}
// 递归检查children
if (variable.children?.length) {
result = result.concat(
findSiblings(variable.children, targetParentId),
);
}
}
return result;
};
const siblings = findSiblings(currentGroup.varInfoList, value.parentId);
if (siblings.some(sibling => sibling.name === value.name)) {
return false;
}
// 2. 检查是否与其他组的根节点重名
// 只有当前节点是根节点时才需要检查
if (!value.parentId) {
const otherGroupsRootNodes = groups
.filter(group => group.groupId !== value.groupId)
.flatMap(group => {
const rootVariableList = group.varInfoList;
const subGroupVarInfoList = group.subGroupList.flatMap(
subGroup => subGroup.varInfoList,
);
return rootVariableList.concat(subGroupVarInfoList);
});
if (otherGroupsRootNodes.some(node => node.name === value.name)) {
return false;
}
}
return true;
},
message: I18n.t('workflow_detail_node_error_variablename_duplicated'),
};
export const existKeywordRules = {
validate: (value: Variable) =>
/^(?!.*\b(true|false|and|AND|or|OR|not|NOT|null|nil|If|Switch)\b)[a-zA-Z_][a-zA-Z_$0-9]*$/.test(
value.name,
),
message: I18n.t('variables_app_name_limit'),
};
export const checkParamNameRules = (
value: Variable,
groups: VariableGroup[],
validateExistKeyword: boolean,
):
| {
valid: boolean;
message: string;
}
| undefined => {
if (!requiredRules.validate(value)) {
return {
valid: false,
message: requiredRules.message,
};
}
if (!duplicateRules.validate(value, groups)) {
return {
valid: false,
message: duplicateRules.message,
};
}
if (
validateExistKeyword &&
!existKeywordRules.validate(value) &&
value.channel === VariableChannel.APP
) {
return {
valid: false,
message: existKeywordRules.message,
};
}
return {
valid: true,
message: '',
};
};

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.
*/
/* eslint-disable @coze-arch/no-deep-relative-import */
import { VariableE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozTrashCan } from '@coze-arch/coze-design/icons';
import { Tooltip, IconButton, Switch } from '@coze-arch/coze-design';
import { ObjectLikeTypes } from '@/store/variable-groups/types';
import { useVariableContext } from '@/context';
import AddOperation from '../add-operation';
import { type TreeNodeCustomData } from '../../../../type';
interface ParamOperatorProps {
data: TreeNodeCustomData;
level: number;
onAppend: () => void;
onDelete: () => void;
onEnabledChange: (enabled: boolean) => void;
hasObjectLike?: boolean;
needRenderAppendChild?: boolean;
readonly?: boolean;
}
export default function ParamOperator({
level,
data,
hasObjectLike,
readonly,
needRenderAppendChild = true,
onEnabledChange,
onDelete,
onAppend,
}: ParamOperatorProps) {
const isLimited = level >= 3;
// 是否可以添加子项
const canAddChild = !readonly && ObjectLikeTypes.includes(data.type);
// 子项按钮是否可用
const enableAddChildButton =
!readonly && hasObjectLike && canAddChild && needRenderAppendChild;
// 是否显示删除按钮
const showDeleteButton = !readonly;
// 是否显示开启/关闭按钮
const enabledSwitch = level === 0;
const { variablePageCanEdit } = useVariableContext();
return (
<div className="flex items-center h-[24px] flex-shrink-0 justify-start gap-x-2 w-[130px]">
{/* 开启/关闭 */}
<Switch
size="small"
disabled={!variablePageCanEdit || !enabledSwitch}
checked={data.enabled}
onChange={onEnabledChange}
/>
{/* 添加子项 */}
{needRenderAppendChild ? (
<div className="flex items-center justify-center">
<Tooltip
content={I18n.t('workflow_detail_node_output_add_subitem')}
theme="dark"
>
<div>
<AddOperation
color="secondary"
disabled={isLimited || !enableAddChildButton}
className="cursor-pointer"
onClick={onAppend}
subitem={true}
/>
</div>
</Tooltip>
</div>
) : null}
{/* 删除 */}
<IconButton
data-testid={VariableE2e.VariableTreeDeleteBtn}
color="secondary"
onClick={onDelete}
disabled={!showDeleteButton}
icon={<IconCozTrashCan />}
/>
</div>
);
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import {
IconCozNumber,
IconCozNumberBracket,
IconCozString,
IconCozStringBracket,
IconCozBoolean,
IconCozBooleanBracket,
IconCozBrace,
IconCozBraceBracket,
} from '@coze-arch/coze-design/icons';
import { ViewVariableType } from '@/store';
export const VARIABLE_TYPE_ICONS_MAP: Record<ViewVariableType, ReactNode> = {
[ViewVariableType.String]: <IconCozString />,
[ViewVariableType.Integer]: <IconCozNumber />,
[ViewVariableType.Boolean]: <IconCozBoolean />,
[ViewVariableType.Number]: <IconCozNumber />,
[ViewVariableType.Object]: <IconCozBrace />,
[ViewVariableType.ArrayString]: <IconCozStringBracket />,
[ViewVariableType.ArrayInteger]: <IconCozNumberBracket />,
[ViewVariableType.ArrayBoolean]: <IconCozBooleanBracket />,
[ViewVariableType.ArrayNumber]: <IconCozNumberBracket />,
[ViewVariableType.ArrayObject]: <IconCozBraceBracket />,
};

View File

@@ -0,0 +1,91 @@
/*
* 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 @coze-arch/no-deep-relative-import */
import React, { useMemo } from 'react';
import { I18n } from '@coze-arch/i18n';
import { type SelectProps } from '@coze-arch/bot-semi/Select';
import { Cascader } from '@coze-arch/coze-design';
import { VARIABLE_TYPE_ALIAS_MAP } from '@/types/view-variable-tree';
import { ReadonlyText } from '../readonly-text';
import { type TreeNodeCustomData } from '../../../../type';
import {
getVariableTypeList,
getCascaderVal,
allVariableTypeList,
} from './utils';
import { VARIABLE_TYPE_ICONS_MAP } from './constants';
interface ParamTypeProps {
data: TreeNodeCustomData;
level: number;
onSelectChange?: SelectProps['onChange'];
readonly?: boolean;
}
export default function ParamType({
data,
onSelectChange,
level,
readonly,
}: ParamTypeProps) {
const optionList = useMemo(() => getVariableTypeList({ level }), [level]);
const cascaderVal = useMemo(
() => getCascaderVal(data.type, allVariableTypeList),
[data.type],
);
return readonly ? (
<ReadonlyText
className="w-full"
value={VARIABLE_TYPE_ALIAS_MAP[data.type]}
/>
) : (
<Cascader
placeholder={I18n.t('workflow_detail_start_variable_type')}
disabled={readonly}
onChange={val => {
let newVal = val;
if (Array.isArray(val)) {
newVal = val[val.length - 1];
}
onSelectChange?.(newVal);
}}
className="w-full coz-stroke-plus"
displayProp="value"
displayRender={selected => {
if (!Array.isArray(selected)) {
return null;
}
return (
<div className="flex items-center gap-1 text-xs">
{VARIABLE_TYPE_ICONS_MAP[selected[selected.length - 1]]}
<div className="truncate">
{VARIABLE_TYPE_ALIAS_MAP[selected[selected.length - 1]]}
</div>
</div>
);
}}
treeData={optionList}
value={cascaderVal}
/>
);
}

View File

@@ -0,0 +1,126 @@
/*
* 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 ReactNode } from 'react';
import { VARIABLE_TYPE_ALIAS_MAP } from '@/types/view-variable-tree';
import { ObjectLikeTypes } from '@/store/variable-groups/types';
import { ViewVariableType } from '@/store';
const LEVEL_LIMIT = 3;
export const generateVariableOption = (
type: ViewVariableType,
label?: string,
display?: string,
) => ({
value: Number(type),
label: label || VARIABLE_TYPE_ALIAS_MAP[type],
display: display || label || VARIABLE_TYPE_ALIAS_MAP[type],
});
export interface VariableTypeOption {
// 类型的值, 非叶子节点时可能为空
value: number | string;
// 选项的展示名称
label: ReactNode;
// 回显的展示名称
display?: string;
// 类型是否禁用
disabled?: boolean;
// 子类型
children?: VariableTypeOption[];
}
export const allVariableTypeList: Array<VariableTypeOption> = [
generateVariableOption(ViewVariableType.String),
generateVariableOption(ViewVariableType.Integer),
generateVariableOption(ViewVariableType.Boolean),
generateVariableOption(ViewVariableType.Number),
generateVariableOption(ViewVariableType.Object),
generateVariableOption(ViewVariableType.ArrayString),
generateVariableOption(ViewVariableType.ArrayInteger),
generateVariableOption(ViewVariableType.ArrayBoolean),
generateVariableOption(ViewVariableType.ArrayNumber),
generateVariableOption(ViewVariableType.ArrayObject),
];
const filterTypes = (
list: Array<VariableTypeOption>,
options?: VariableListOptions,
): Array<VariableTypeOption> => {
const { level } = options || {};
return list.reduce((pre, cur) => {
const newOption = { ...cur };
if (newOption.children) {
newOption.children = filterTypes(newOption.children, options);
}
/**
* 1. 到达层级限制时禁用 ObjectLike 类型,避免嵌套过深
*/
const disabled = Boolean(
level &&
level >= LEVEL_LIMIT &&
ObjectLikeTypes.includes(Number(newOption.value)),
);
return [
...pre,
{
...newOption,
disabled,
},
];
}, [] as Array<VariableTypeOption>);
};
interface VariableListOptions {
level?: number;
}
export const getVariableTypeList = options =>
filterTypes(allVariableTypeList, options);
/**
* 获取类型在选项列表中的路径,作为 cascader 的 value
*/
export const getCascaderVal = (
originalVal: ViewVariableType,
list: Array<VariableTypeOption>,
path: Array<string | number> = [],
) => {
let valuePath = [...path];
list.forEach(item => {
if (item.children) {
const childPath = getCascaderVal(originalVal, item.children, [
...valuePath,
item.value,
]);
if (childPath[childPath.length - 1] === originalVal) {
valuePath = childPath;
return;
}
} else if (item.value === originalVal) {
valuePath.push(originalVal);
return;
}
});
return valuePath;
};

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from 'classnames';
import { Typography } from '@coze-arch/coze-design';
const { Text } = Typography;
export const ReadonlyText = (props: { value: string; className?: string }) => {
const { value, className } = props;
return (
<Text
className={classNames(
'w-full coz-fg-primary text-sm !font-medium',
className,
)}
ellipsis
>
{value}
</Text>
);
};

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ViewVariableType } from '@/store';
export enum ChangeMode {
Update,
Delete,
Append,
UpdateEnabled,
Replace,
}
// JSON类型
// eslint-disable-next-line @typescript-eslint/naming-convention
export const JSONLikeTypes = [
ViewVariableType.Object,
ViewVariableType.ArrayObject,
ViewVariableType.ArrayBoolean,
ViewVariableType.ArrayNumber,
ViewVariableType.ArrayString,
ViewVariableType.ArrayInteger,
];

View File

@@ -0,0 +1,202 @@
/*
* 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 @coze-arch/max-line-per-function */
import React, { useCallback, useRef } from 'react';
import isNumber from 'lodash-es/isNumber';
import { cloneDeep } from 'lodash-es';
import classNames from 'classnames';
import { type RenderFullLabelProps } from '@coze-arch/bot-semi/Tree';
import { IconCozArrowRight } from '@coze-arch/coze-design/icons';
import { type Variable, type ViewVariableType } from '@/store';
import { type TreeNodeCustomData } from '../../type';
import { TreeIndentWidth } from '../../constants';
import { ChangeMode } from './constants';
import ParamType from './components/param-type';
import ParamOperator from './components/param-operator';
import { ParamName } from './components/param-name';
import { ParamDescription } from './components/param-description';
import { ParamDefault } from './components/param-default';
import { ParamChannel } from './components/param-channel';
export interface CustomTreeNodeProps extends RenderFullLabelProps {
level: number;
readonly?: boolean;
variablePageCanEdit?: boolean;
needRenderAppendChild?: boolean;
onChange: (mode: ChangeMode, param: TreeNodeCustomData) => void;
hasObjectLike?: boolean;
disableDelete?: boolean;
couldCollapse?: boolean;
hideHeaderKeys?: string[];
collapsed?: boolean;
validateExistKeyword?: boolean;
onCollapse?: (collapsed: boolean) => void;
}
export default function CustomTreeNode(props: CustomTreeNodeProps) {
const {
data,
className,
level,
readonly = false,
onChange,
hasObjectLike,
couldCollapse = true,
hideHeaderKeys,
collapsed = false,
onCollapse,
validateExistKeyword = false,
} = props;
// 当前值
const value = cloneDeep(data) as Variable;
const treeNodeRef = useRef<HTMLDivElement>(null);
// 删除时
const onDelete = () => {
onChange(ChangeMode.Delete, value);
};
// 新增子项时
const onAppend = () => {
onChange(ChangeMode.Append, value);
};
// 类型切换时
const onSelectChange = (
val?: string | number | Array<unknown> | Record<string, unknown>,
) => {
if (val === undefined) {
return;
}
if (!isNumber(val)) {
return;
}
// 清除默认值
value.defaultValue = '';
value.children = [];
onChange(ChangeMode.Update, { ...value, type: val as ViewVariableType });
};
const onDefaultChange = (
val: string | number | boolean | TreeNodeCustomData,
) => {
onChange(ChangeMode.Update, { ...value, defaultValue: val.toString() });
};
const onImportChange = (val: TreeNodeCustomData) => {
onChange(ChangeMode.Replace, val);
};
const onNameChange = (name: string) => {
if (value.name === name) {
return;
}
onChange(ChangeMode.Update, { ...value, name });
};
const onDescriptionChange = useCallback(
(description: string) => {
if (value.description === description) {
return;
}
onChange(ChangeMode.Update, { ...value, description });
},
[onChange, value],
);
const onEnabledChange = useCallback(
(enabled: boolean) => {
onChange(ChangeMode.UpdateEnabled, { ...value, enabled });
},
[onChange, value],
);
return (
<div
className={classNames('flex items-center', {
[className]: Boolean(className),
})}
ref={treeNodeRef}
>
<div className="flex flex-1 my-3 gap-x-4 items-center w-full relative h-[32px]">
<div className="flex flex-1 items-center flex-nowrap overflow-x-hidden overflow-y-visible">
<div
className="flex items-center justify-end"
style={{ width: level * TreeIndentWidth }}
></div>
<IconCozArrowRight
className={classNames(
'flex-none mr-2 w-[16px] h-[16px]',
collapsed ? 'rotate-90' : '',
couldCollapse ? '' : 'invisible',
'cursor-pointer',
level === 0 && !couldCollapse ? 'hidden' : '',
)}
onClick={() => {
onCollapse?.(!collapsed);
}}
/>
<ParamName
readonly={readonly}
data={value}
onChange={onNameChange}
validateExistKeyword={validateExistKeyword}
/>
</div>
<div className="flex-1 overflow-hidden">
<ParamDescription
data={value}
onChange={onDescriptionChange}
readonly={readonly}
/>
</div>
{!hideHeaderKeys?.includes('type') ? (
<div className="flex-none w-[166px] basis-[166px]">
<ParamType
level={level}
readonly={readonly}
data={value}
onSelectChange={onSelectChange}
/>
</div>
) : null}
<div className="flex-none w-[164px] basis-[164px]">
<ParamDefault
readonly={readonly}
data={value}
onDefaultChange={onDefaultChange}
onImportChange={onImportChange}
/>
</div>
<div className="flex-none w-[164px] basis-[164px] empty:hidden">
<ParamChannel value={value} />
</div>
<div className="flex-none w-[130px] basis-[130px]">
<ParamOperator
data={value}
readonly={readonly}
level={level}
onDelete={onDelete}
onAppend={onAppend}
hasObjectLike={hasObjectLike}
needRenderAppendChild={!hideHeaderKeys?.includes('type')}
onEnabledChange={onEnabledChange}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,227 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
useCallback,
useEffect,
useState,
Suspense,
lazy,
type FC,
useMemo,
} from 'react';
import { I18n } from '@coze-arch/i18n';
import { IconCozBroom } from '@coze-arch/coze-design/icons';
import { IconButton, Tooltip, Modal } from '@coze-arch/coze-design';
import { MAX_JSON_LENGTH } from '../../constants';
import { formatJson } from './utils/format-json';
import {
convertSchemaService,
type SchemaNode,
} from './service/convert-schema-service';
import lightStyles from './light.module.less';
const LazyBizIDEMonacoEditor = lazy(async () => {
const { Editor } = await import('@coze-arch/bot-monaco-editor');
return { default: Editor };
});
const BizIDEMonacoEditor = props => (
<Suspense>
<LazyBizIDEMonacoEditor {...props} />
</Suspense>
);
interface JSONEditorProps {
id: string;
value: string;
groupId: string;
setValue: (value: string) => void;
visible: boolean;
readonly?: boolean;
onCancel: () => void;
onOk: (value: SchemaNode[]) => void;
}
const ValidateRules = {
jsonValid: {
message: I18n.t('variables_json_input_error'),
validator: (value: string) => {
try {
const rs = JSON.parse(value);
const isJson = typeof rs === 'object';
return isJson;
// eslint-disable-next-line @coze-arch/use-error-in-catch
} catch (error) {
return false;
}
},
},
jsonLength: {
message: I18n.t('variables_json_input_limit'),
validator: (value: string) => {
if (value.length > MAX_JSON_LENGTH) {
return false;
}
return true;
},
},
};
export const JSONEditor: FC<JSONEditorProps> = props => {
const { id, value, setValue, visible, onCancel, onOk, readonly } = props;
const [schema, setSchema] = useState<SchemaNode[] | undefined>();
const [error, setError] = useState<string | undefined>();
const change = useCallback(async () => {
if (!schema) {
return;
}
setError(undefined);
return new Promise(resolve => {
Modal.warning({
title: I18n.t('workflow_json_node_update_tips_title'),
content: I18n.t('workflow_json_node_update_tips_content'),
okType: 'warning',
okText: I18n.t('Confirm'),
cancelText: I18n.t('Cancel'),
onOk: () => {
const outputValue = convert(value) || [];
onOk(outputValue);
resolve(true);
},
onCancel: () => resolve(false),
});
});
}, [schema]);
const convert = (jsonString: string): SchemaNode[] | undefined => {
if (!jsonString) {
return;
}
try {
const json = JSON.parse(jsonString);
const outputValue = convertSchemaService(json);
if (
!outputValue ||
!Array.isArray(outputValue) ||
outputValue.length === 0
) {
return;
}
return outputValue;
} catch (e) {
return;
}
};
const validate = (newValue: string) => {
const rules = Object.values(ValidateRules);
for (const rule of rules) {
if (!rule.validator(newValue)) {
setError(rule.message);
return false;
}
}
setError(undefined);
return true;
};
const isValid = useMemo(() => validate(value), [value]);
// 同步 value 和 schema
useEffect(() => {
const _schema = convert(value);
setSchema(_schema);
}, [value]);
return (
<Modal
visible={visible}
title={
readonly
? I18n.t('variables_json_input_readonly_title')
: I18n.t('workflow_json_windows_title')
}
okText={I18n.t('Confirm')}
cancelText={I18n.t('Cancel')}
onOk={change}
onCancel={onCancel}
height={530}
okButtonProps={{
disabled: !isValid || readonly,
}}
>
<div key={id} className="w-full relative">
<div className="w-full h-[48px] coz-bg-primary rounded-t-lg coz-fg-primary font-medium text-sm flex items-center justify-between px-4">
<div className="coz-fg-primary">JSON</div>
<Tooltip content={I18n.t('workflow_exception_ignore_format')}>
<IconButton
className="bg-transparent"
disabled={readonly}
icon={<IconCozBroom />}
onClick={() => {
setValue(formatJson(value));
}}
/>
</Tooltip>
</div>
<div className="w-full h-[320px]">
<BizIDEMonacoEditor
key={id}
value={value}
defaultLanguage="json"
/** 通过 css 样式覆盖 icube-dark 主题 */
className={lightStyles.light}
options={{
fontSize: 13,
minimap: {
enabled: false,
},
contextmenu: false,
scrollbar: {
verticalScrollbarSize: 10,
alwaysConsumeMouseWheel: false,
},
lineNumbers: 'on',
lineNumbersMinChars: 3,
folding: false,
lineDecorationsWidth: 2,
renderLineHighlight: 'none',
glyphMargin: false,
scrollBeyondLastLine: false,
overviewRulerBorder: false,
wordWrap: 'on',
fixedOverflowWidgets: true,
readOnly: readonly,
}}
onChange={stringValue => {
setValue(stringValue || '');
}}
/>
</div>
{error ? (
<div className="absolute top-full">
<span className="coz-fg-hglt-red text-[12px] font-[400] leading-[16px] whitespace-nowrap">
{error}
</span>
</div>
) : null}
</div>
</Modal>
);
};

View File

@@ -0,0 +1,399 @@
// fork from biz-ide-component vs-light-theme.module.less
// 样式与 ide 保持一致
/* stylelint-disable declaration-no-important */
.light {
:global {
.monaco-editor {
--vscode-foreground: #616161;
--vscode-disabledForeground: rgba(97, 97, 97, 50%);
--vscode-errorForeground: #a1260d;
--vscode-descriptionForeground: #717171;
--vscode-icon-foreground: #424242;
--vscode-focusBorder: #0090f1;
--vscode-textSeparator-foreground: rgba(0, 0, 0, 18%);
--vscode-textLink-foreground: #006ab1;
--vscode-textLink-activeForeground: #006ab1;
--vscode-textPreformat-foreground: #a31515;
--vscode-textBlockQuote-background: rgba(127, 127, 127, 10%);
--vscode-textBlockQuote-border: rgba(0, 122, 204, 50%);
--vscode-textCodeBlock-background: rgba(220, 220, 220, 40%);
--vscode-widget-shadow: rgba(0, 0, 0, 16%);
--vscode-input-background: #fff;
--vscode-input-foreground: #616161;
--vscode-inputOption-activeBorder: #007acc;
--vscode-inputOption-hoverBackground: rgba(184, 184, 184, 31%);
--vscode-inputOption-activeBackground: rgba(0, 144, 241, 20%);
--vscode-inputOption-activeForeground: #000;
--vscode-input-placeholderForeground: rgba(97, 97, 97, 50%);
--vscode-inputValidation-infoBackground: #d6ecf2;
--vscode-inputValidation-infoBorder: #007acc;
--vscode-inputValidation-warningBackground: #f6f5d2;
--vscode-inputValidation-warningBorder: #b89500;
--vscode-inputValidation-errorBackground: #f2dede;
--vscode-inputValidation-errorBorder: #be1100;
--vscode-dropdown-background: #fff;
--vscode-dropdown-foreground: #616161;
--vscode-dropdown-border: #cecece;
--vscode-button-foreground: #fff;
--vscode-button-separator: rgba(255, 255, 255, 40%);
--vscode-button-background: #007acc;
--vscode-button-hoverBackground: #0062a3;
--vscode-button-secondaryForeground: #fff;
--vscode-button-secondaryBackground: #5f6a79;
--vscode-button-secondaryHoverBackground: #4c5561;
--vscode-badge-background: #c4c4c4;
--vscode-badge-foreground: #333;
--vscode-scrollbar-shadow: #ddd;
--vscode-scrollbarSlider-background: rgba(100, 100, 100, 40%);
--vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 70%);
--vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 60%);
--vscode-progressBar-background: #0e70c0;
--vscode-editorError-foreground: #e51400;
--vscode-editorWarning-foreground: #bf8803;
--vscode-editorInfo-foreground: #1a85ff;
--vscode-editorHint-foreground: #6c6c6c;
--vscode-sash-hoverBorder: #0090f1;
--vscode-editor-background: #fffffe;
--vscode-editor-foreground: #000;
--vscode-editorStickyScroll-background: #fffffe;
--vscode-editorStickyScrollHover-background: #f0f0f0;
--vscode-editorWidget-background: #f3f3f3;
--vscode-editorWidget-foreground: #616161;
--vscode-editorWidget-border: #c8c8c8;
--vscode-quickInput-background: #f3f3f3;
--vscode-quickInput-foreground: #616161;
--vscode-quickInputTitle-background: rgba(0, 0, 0, 6%);
--vscode-pickerGroup-foreground: #0066bf;
--vscode-pickerGroup-border: #cccedb;
--vscode-keybindingLabel-background: rgba(221, 221, 221, 40%);
--vscode-keybindingLabel-foreground: #555;
--vscode-keybindingLabel-border: rgba(204, 204, 204, 40%);
--vscode-keybindingLabel-bottomBorder: rgba(187, 187, 187, 40%);
--vscode-editor-selectionBackground: #add6ff;
--vscode-editor-inactiveSelectionBackground: #e5ebf1;
--vscode-editor-selectionHighlightBackground: rgba(173, 214, 255, 30%);
--vscode-editor-findMatchBackground: #a8ac94;
--vscode-editor-findMatchHighlightBackground: rgba(234, 92, 0, 33%);
--vscode-editor-findRangeHighlightBackground: rgba(180, 180, 180, 30%);
--vscode-searchEditor-findMatchBackground: rgba(234, 92, 0, 22%);
--vscode-search-resultsInfoForeground: #616161;
--vscode-editor-hoverHighlightBackground: rgba(173, 214, 255, 15%);
--vscode-editorHoverWidget-background: #f3f3f3;
--vscode-editorHoverWidget-foreground: #616161;
--vscode-editorHoverWidget-border: #c8c8c8;
--vscode-editorHoverWidget-statusBarBackground: #e7e7e7;
--vscode-editorLink-activeForeground: #00f;
--vscode-editorInlayHint-foreground: #969696;
--vscode-editorInlayHint-background: rgba(196, 196, 196, 10%);
--vscode-editorInlayHint-typeForeground: #969696;
--vscode-editorInlayHint-typeBackground: rgba(196, 196, 196, 10%);
--vscode-editorInlayHint-parameterForeground: #969696;
--vscode-editorInlayHint-parameterBackground: rgba(196, 196, 196, 10%);
--vscode-editorLightBulb-foreground: #ddb100;
--vscode-editorLightBulbAutoFix-foreground: #007acc;
--vscode-diffEditor-insertedTextBackground: rgba(156, 204, 44, 25%);
--vscode-diffEditor-removedTextBackground: rgba(255, 0, 0, 20%);
--vscode-diffEditor-insertedLineBackground: rgba(155, 185, 85, 20%);
--vscode-diffEditor-removedLineBackground: rgba(255, 0, 0, 20%);
--vscode-diffEditor-diagonalFill: rgba(34, 34, 34, 20%);
--vscode-diffEditor-unchangedRegionBackground: #e4e4e4;
--vscode-diffEditor-unchangedRegionForeground: #4d4c4c;
--vscode-diffEditor-unchangedCodeBackground: rgba(184, 184, 184, 16%);
--vscode-list-focusOutline: #0090f1;
--vscode-list-activeSelectionBackground: #0060c0;
--vscode-list-activeSelectionForeground: #fff;
--vscode-list-inactiveSelectionBackground: #e4e6f1;
--vscode-list-hoverBackground: #f0f0f0;
--vscode-list-dropBackground: #d6ebff;
--vscode-list-highlightForeground: #0066bf;
--vscode-list-focusHighlightForeground: #bbe7ff;
--vscode-list-invalidItemForeground: #b89500;
--vscode-list-errorForeground: #b01011;
--vscode-list-warningForeground: #855f00;
--vscode-listFilterWidget-background: #f3f3f3;
--vscode-listFilterWidget-outline: rgba(0, 0, 0, 0%);
--vscode-listFilterWidget-noMatchesOutline: #be1100;
--vscode-listFilterWidget-shadow: rgba(0, 0, 0, 16%);
--vscode-list-filterMatchBackground: rgba(234, 92, 0, 33%);
--vscode-tree-indentGuidesStroke: #a9a9a9;
--vscode-tree-inactiveIndentGuidesStroke: rgba(169, 169, 169, 40%);
--vscode-tree-tableColumnsBorder: rgba(97, 97, 97, 13%);
--vscode-tree-tableOddRowsBackground: rgba(97, 97, 97, 4%);
--vscode-list-deemphasizedForeground: #8e8e90;
--vscode-checkbox-background: #fff;
--vscode-checkbox-selectBackground: #f3f3f3;
--vscode-checkbox-foreground: #616161;
--vscode-checkbox-border: #cecece;
--vscode-checkbox-selectBorder: #424242;
--vscode-quickInputList-focusForeground: #fff;
--vscode-quickInputList-focusBackground: #0060c0;
--vscode-menu-foreground: #616161;
--vscode-menu-background: #fff;
--vscode-menu-selectionForeground: #fff;
--vscode-menu-selectionBackground: #0060c0;
--vscode-menu-separatorBackground: #d4d4d4;
--vscode-toolbar-hoverBackground: rgba(184, 184, 184, 31%);
--vscode-toolbar-activeBackground: rgba(166, 166, 166, 31%);
--vscode-editor-snippetTabstopHighlightBackground: rgba(10, 50, 100, 20%);
--vscode-editor-snippetFinalTabstopHighlightBorder: rgba(10,
50,
100,
50%);
--vscode-breadcrumb-foreground: rgba(97, 97, 97, 80%);
--vscode-breadcrumb-background: #fffffe;
--vscode-breadcrumb-focusForeground: #4e4e4e;
--vscode-breadcrumb-activeSelectionForeground: #4e4e4e;
--vscode-breadcrumbPicker-background: #f3f3f3;
--vscode-merge-currentHeaderBackground: rgba(64, 200, 174, 50%);
--vscode-merge-currentContentBackground: rgba(64, 200, 174, 20%);
--vscode-merge-incomingHeaderBackground: rgba(64, 166, 255, 50%);
--vscode-merge-incomingContentBackground: rgba(64, 166, 255, 20%);
--vscode-merge-commonHeaderBackground: rgba(96, 96, 96, 40%);
--vscode-merge-commonContentBackground: rgba(96, 96, 96, 16%);
--vscode-editorOverviewRuler-currentContentForeground: rgba(64,
200,
174,
50%);
--vscode-editorOverviewRuler-incomingContentForeground: rgba(64,
166,
255,
50%);
--vscode-editorOverviewRuler-commonContentForeground: rgba(96,
96,
96,
40%);
--vscode-editorOverviewRuler-findMatchForeground: rgba(209,
134,
22,
49%);
--vscode-editorOverviewRuler-selectionHighlightForeground: rgba(160,
160,
160,
80%);
--vscode-minimap-findMatchHighlight: #d18616;
--vscode-minimap-selectionOccurrenceHighlight: #c9c9c9;
--vscode-minimap-selectionHighlight: #add6ff;
--vscode-minimap-errorHighlight: rgba(255, 18, 18, 70%);
--vscode-minimap-warningHighlight: #bf8803;
--vscode-minimap-foregroundOpacity: #000;
--vscode-minimapSlider-background: rgba(100, 100, 100, 20%);
--vscode-minimapSlider-hoverBackground: rgba(100, 100, 100, 35%);
--vscode-minimapSlider-activeBackground: rgba(0, 0, 0, 30%);
--vscode-problemsErrorIcon-foreground: #e51400;
--vscode-problemsWarningIcon-foreground: #bf8803;
--vscode-problemsInfoIcon-foreground: #1a85ff;
--vscode-charts-foreground: #616161;
--vscode-charts-lines: rgba(97, 97, 97, 50%);
--vscode-charts-red: #e51400;
--vscode-charts-blue: #1a85ff;
--vscode-charts-yellow: #bf8803;
--vscode-charts-orange: #d18616;
--vscode-charts-green: #388a34;
--vscode-charts-purple: #652d90;
--vscode-diffEditor-move-border: rgba(139, 139, 139, 61%);
--vscode-diffEditor-moveActive-border: #ffa500;
--vscode-symbolIcon-arrayForeground: #616161;
--vscode-symbolIcon-booleanForeground: #616161;
--vscode-symbolIcon-classForeground: #d67e00;
--vscode-symbolIcon-colorForeground: #616161;
--vscode-symbolIcon-constantForeground: #616161;
--vscode-symbolIcon-constructorForeground: #652d90;
--vscode-symbolIcon-enumeratorForeground: #d67e00;
--vscode-symbolIcon-enumeratorMemberForeground: #007acc;
--vscode-symbolIcon-eventForeground: #d67e00;
--vscode-symbolIcon-fieldForeground: #007acc;
--vscode-symbolIcon-fileForeground: #616161;
--vscode-symbolIcon-folderForeground: #616161;
--vscode-symbolIcon-functionForeground: #652d90;
--vscode-symbolIcon-interfaceForeground: #007acc;
--vscode-symbolIcon-keyForeground: #616161;
--vscode-symbolIcon-keywordForeground: #616161;
--vscode-symbolIcon-methodForeground: #652d90;
--vscode-symbolIcon-moduleForeground: #616161;
--vscode-symbolIcon-namespaceForeground: #616161;
--vscode-symbolIcon-nullForeground: #616161;
--vscode-symbolIcon-numberForeground: #616161;
--vscode-symbolIcon-objectForeground: #616161;
--vscode-symbolIcon-operatorForeground: #616161;
--vscode-symbolIcon-packageForeground: #616161;
--vscode-symbolIcon-propertyForeground: #616161;
--vscode-symbolIcon-referenceForeground: #616161;
--vscode-symbolIcon-snippetForeground: #616161;
--vscode-symbolIcon-stringForeground: #616161;
--vscode-symbolIcon-structForeground: #616161;
--vscode-symbolIcon-textForeground: #616161;
--vscode-symbolIcon-typeParameterForeground: #616161;
--vscode-symbolIcon-unitForeground: #616161;
--vscode-symbolIcon-variableForeground: #007acc;
--vscode-actionBar-toggledBackground: rgba(0, 144, 241, 20%);
--vscode-editor-lineHighlightBorder: #eee;
--vscode-editor-rangeHighlightBackground: rgba(253, 255, 0, 20%);
--vscode-editor-symbolHighlightBackground: rgba(234, 92, 0, 33%);
--vscode-editorCursor-foreground: #000;
--vscode-editorWhitespace-foreground: rgba(51, 51, 51, 20%);
--vscode-editorLineNumber-foreground: #237893;
--vscode-editorIndentGuide-background: rgba(51, 51, 51, 20%);
--vscode-editorIndentGuide-activeBackground: rgba(51, 51, 51, 20%);
--vscode-editorIndentGuide-background1: #d3d3d3;
--vscode-editorIndentGuide-background2: rgba(0, 0, 0, 0%);
--vscode-editorIndentGuide-background3: rgba(0, 0, 0, 0%);
--vscode-editorIndentGuide-background4: rgba(0, 0, 0, 0%);
--vscode-editorIndentGuide-background5: rgba(0, 0, 0, 0%);
--vscode-editorIndentGuide-background6: rgba(0, 0, 0, 0%);
--vscode-editorIndentGuide-activeBackground1: #939393;
--vscode-editorIndentGuide-activeBackground2: rgba(0, 0, 0, 0%);
--vscode-editorIndentGuide-activeBackground3: rgba(0, 0, 0, 0%);
--vscode-editorIndentGuide-activeBackground4: rgba(0, 0, 0, 0%);
--vscode-editorIndentGuide-activeBackground5: rgba(0, 0, 0, 0%);
--vscode-editorIndentGuide-activeBackground6: rgba(0, 0, 0, 0%);
--vscode-editorActiveLineNumber-foreground: #0b216f;
--vscode-editorLineNumber-activeForeground: #0b216f;
--vscode-editorRuler-foreground: #d3d3d3;
--vscode-editorCodeLens-foreground: #919191;
--vscode-editorBracketMatch-background: rgba(0, 100, 0, 10%);
--vscode-editorBracketMatch-border: #b9b9b9;
--vscode-editorOverviewRuler-border: rgba(127, 127, 127, 30%);
--vscode-editorGutter-background: #fffffe;
--vscode-editorUnnecessaryCode-opacity: rgba(0, 0, 0, 47%);
--vscode-editorGhostText-foreground: rgba(0, 0, 0, 47%);
--vscode-editorOverviewRuler-rangeHighlightForeground: rgba(0,
122,
204,
60%);
--vscode-editorOverviewRuler-errorForeground: rgba(255, 18, 18, 70%);
--vscode-editorOverviewRuler-warningForeground: #bf8803;
--vscode-editorOverviewRuler-infoForeground: #1a85ff;
--vscode-editorBracketHighlight-foreground1: #0431fa;
--vscode-editorBracketHighlight-foreground2: #319331;
--vscode-editorBracketHighlight-foreground3: #7b3814;
--vscode-editorBracketHighlight-foreground4: rgba(0, 0, 0, 0%);
--vscode-editorBracketHighlight-foreground5: rgba(0, 0, 0, 0%);
--vscode-editorBracketHighlight-foreground6: rgba(0, 0, 0, 0%);
--vscode-editorBracketHighlight-unexpectedBracket-foreground: rgba(255,
18,
18,
80%);
--vscode-editorBracketPairGuide-background1: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-background2: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-background3: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-background4: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-background5: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-background6: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-activeBackground1: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-activeBackground2: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-activeBackground3: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-activeBackground4: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-activeBackground5: rgba(0, 0, 0, 0%);
--vscode-editorBracketPairGuide-activeBackground6: rgba(0, 0, 0, 0%);
--vscode-editorUnicodeHighlight-border: #cea33d;
--vscode-editorUnicodeHighlight-background: rgba(206, 163, 61, 8%);
--vscode-editorOverviewRuler-bracketMatchForeground: #a0a0a0;
--vscode-editor-linkedEditingBackground: rgba(255, 0, 0, 30%);
--vscode-editor-wordHighlightBackground: rgba(87, 87, 87, 25%);
--vscode-editor-wordHighlightStrongBackground: rgba(14, 99, 156, 25%);
--vscode-editor-wordHighlightTextBackground: rgba(87, 87, 87, 25%);
--vscode-editorOverviewRuler-wordHighlightForeground: rgba(160,
160,
160,
80%);
--vscode-editorOverviewRuler-wordHighlightStrongForeground: rgba(192,
160,
192,
80%);
--vscode-editorOverviewRuler-wordHighlightTextForeground: rgba(160,
160,
160,
80%);
--vscode-peekViewTitle-background: #f3f3f3;
--vscode-peekViewTitleLabel-foreground: #000;
--vscode-peekViewTitleDescription-foreground: #616161;
--vscode-peekView-border: #1a85ff;
--vscode-peekViewResult-background: #f3f3f3;
--vscode-peekViewResult-lineForeground: #646465;
--vscode-peekViewResult-fileForeground: #1e1e1e;
--vscode-peekViewResult-selectionBackground: rgba(51, 153, 255, 20%);
--vscode-peekViewResult-selectionForeground: #6c6c6c;
--vscode-peekViewEditor-background: #f2f8fc;
--vscode-peekViewEditorGutter-background: #f2f8fc;
--vscode-peekViewEditorStickyScroll-background: #f2f8fc;
--vscode-peekViewResult-matchHighlightBackground: rgba(234, 92, 0, 30%);
--vscode-peekViewEditor-matchHighlightBackground: rgba(245, 216, 2, 87%);
--vscode-editorMarkerNavigationError-background: #e51400;
--vscode-editorMarkerNavigationError-headerBackground: rgba(229,
20,
0,
10%);
--vscode-editorMarkerNavigationWarning-background: #bf8803;
--vscode-editorMarkerNavigationWarning-headerBackground: rgba(191,
136,
3,
10%);
--vscode-editorMarkerNavigationInfo-background: #1a85ff;
--vscode-editorMarkerNavigationInfo-headerBackground: rgba(26,
133,
255,
10%);
--vscode-editorMarkerNavigation-background: #fffffe;
--vscode-editorHoverWidget-highlightForeground: #0066bf;
--vscode-editorSuggestWidget-background: #f3f3f3;
--vscode-editorSuggestWidget-border: #c8c8c8;
--vscode-editorSuggestWidget-foreground: #000;
--vscode-editorSuggestWidget-selectedForeground: #fff;
--vscode-editorSuggestWidget-selectedBackground: #0060c0;
--vscode-editorSuggestWidget-highlightForeground: #0066bf;
--vscode-editorSuggestWidget-focusHighlightForeground: #bbe7ff;
--vscode-editorSuggestWidgetStatus-foreground: rgba(0, 0, 0, 50%);
--vscode-editor-foldBackground: rgba(173, 214, 255, 30%);
--vscode-editorGutter-foldingControlForeground: #424242;
background-color: #fff;
.bracket-highlighting-0 {
color: #d5a00d; // { }
}
.bracket-highlighting-1 {
color: #8140e3; // [ ]
}
.mtk1 {
color: #000;
}
.lines-content .core-guide-indent {
box-shadow: 1px 0 0 0 transparent !important;
}
.lines-content .core-guide-indent.indent-active {
box-shadow: 1px 0 0 0 transparent !important;
}
.line-numbers {
color: #A7A7B0;
}
.marker-widget {
background-color: #fff !important;
}
.find-widget {
// 搜索框
color: #000;
background-color: #fff;
.monaco-sash {
background-color: #fff;
}
.button:not(.disabled):hover,
.monaco-editor .find-widget .codicon-find-selection:hover {
background-color: #f8f8f8 !important;
}
.monaco-inputbox {
border: 1px solid #e4e3e4 !important;
}
}
}
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface SchemaNode {
name: string;
type: number;
children?: SchemaNode[];
defaultValue: string;
}
// modify from @byted/biz-ide-component
export const convertSchemaService = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any,
maxDepth = 20,
currentDepth = 1,
): SchemaNode[] => {
if (currentDepth > maxDepth) {
return [];
}
const paramSchema: SchemaNode[] = [];
Object.keys(object).forEach(key => {
const value = object[key];
switch (typeof value) {
case 'string':
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 1 /* String */,
});
break;
case 'number':
if (Number.isInteger(value)) {
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 2 /* Integer */,
});
} else {
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 4 /* Number */,
});
}
break;
case 'boolean':
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 3 /* Boolean */,
});
break;
case 'object':
if (value === null) {
break;
}
if (Array.isArray(value)) {
if (value.length > 0) {
switch (typeof value[0]) {
case 'string':
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 99 /* ArrayString */,
});
break;
case 'number':
if (Number.isInteger(value[0])) {
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 100 /* ArrayInteger */,
});
} else {
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 102 /* ArrayNumber */,
});
}
break;
case 'boolean':
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 101 /* ArrayBoolean */,
});
break;
case 'object':
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 103 /* ArrayObject */,
children: convertSchemaService(
value[0],
maxDepth,
currentDepth + 1,
),
});
break;
default:
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 99 /* ArrayString */,
});
}
} else {
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 99 /* ArrayString */,
});
}
} else {
paramSchema.push({
name: key,
defaultValue: JSON.stringify(value),
type: 6 /* Object */,
children: convertSchemaService(value, maxDepth, currentDepth + 1),
});
}
break;
default:
throw new Error('ContainsInvalidValue');
}
});
return paramSchema;
};

View File

@@ -0,0 +1,24 @@
/*
* 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.
*/
const INDENT = 4;
export const formatJson = (json: string) => {
try {
return JSON.stringify(JSON.parse(json), null, INDENT);
} catch (e) {
return json;
}
};

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 { useEffect, useState, type FC } from 'react';
import { merge, cloneDeep } from 'lodash-es';
import { type SchemaNode } from '../json-editor/service/convert-schema-service';
import { JSONEditor } from '../json-editor';
import type { TreeNodeCustomData } from '../../type';
import {
MAX_LEVEL,
MAX_NAME_LENGTH,
MAX_JSON_VARIABLE_COUNT,
} from '../../constants';
import { cutOffInvalidData } from './utils/cut-off';
import { exportVariableService } from './services/use-case-service/export-variable-service';
import { getEditorViewVariableJson } from './services/life-cycle-service/init-service';
interface JSONImportProps {
visible: boolean;
onCancel: () => void;
treeData: TreeNodeCustomData;
rules: {
jsonImport: boolean;
readonly: boolean;
};
onOk: (value: TreeNodeCustomData) => void;
}
export const JSONImport: FC<JSONImportProps> = props => {
const { treeData, rules, visible, onCancel, onOk } = props;
const { jsonImport, readonly } = rules;
const [jsonString, setJsonString] = useState('');
const handleImport = (data: SchemaNode[]) => {
const allowDepth = MAX_LEVEL; // 最大深度限制
const allowNameLength = MAX_NAME_LENGTH; // 名称长度限制
const maxVariableCount = MAX_JSON_VARIABLE_COUNT; // 最大变量数量限制
const variables = exportVariableService(
data,
{
groupId: treeData.groupId,
channel: treeData.channel,
},
treeData, // 传入原始变量以保持variableId
);
// 裁切非法数据
const dataCutoff = cutOffInvalidData({
data: variables,
allowDepth,
allowNameLength,
maxVariableCount,
});
// 先深拷贝原始数据
const clonedTreeData = cloneDeep(treeData);
// 合并新旧数据
const mergedData = merge(clonedTreeData, dataCutoff[0]);
// 更新数据
return onOk(mergedData);
};
useEffect(() => {
setJsonString(getEditorViewVariableJson(treeData));
}, [treeData]);
if (!jsonImport) {
return <></>;
}
return (
<JSONEditor
id={treeData.variableId}
groupId={treeData.groupId}
value={jsonString}
readonly={readonly}
setValue={(value: string) => {
setJsonString(value);
}}
visible={visible}
onOk={handleImport}
onCancel={onCancel}
/>
);
};

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ViewVariableType } from '@/store/variable-groups/types';
import { type TreeNodeCustomData } from '@/components/variable-tree/type';
import { formatJson } from '@/components/variable-tree/components/json-editor/utils/format-json';
const getDefaultValueByType = (type: ViewVariableType) => {
switch (type) {
case ViewVariableType.String:
return '';
case ViewVariableType.Integer:
case ViewVariableType.Number:
return 0;
case ViewVariableType.Boolean:
return false;
case ViewVariableType.Object:
return {};
case ViewVariableType.ArrayString:
return [''];
case ViewVariableType.ArrayInteger:
return [0];
case ViewVariableType.ArrayBoolean:
return [true];
case ViewVariableType.ArrayNumber:
return [0];
case ViewVariableType.ArrayObject:
return [{}];
default:
return {};
}
};
const isArrayType = (type: ViewVariableType) =>
[
ViewVariableType.ArrayString,
ViewVariableType.ArrayInteger,
ViewVariableType.ArrayBoolean,
ViewVariableType.ArrayNumber,
ViewVariableType.ArrayObject,
].includes(type);
export const getEditorViewVariableJson = (treeData: TreeNodeCustomData) => {
const { defaultValue, type, name, children } = treeData;
if (defaultValue) {
const json = JSON.parse(defaultValue);
return formatJson(
JSON.stringify({
[name]: json,
}),
);
}
// 如果没有name,返回空对象
if (!name) {
return '{}';
}
const isArray = isArrayType(type);
// 递归处理children
const processChildren = (
nodes?: TreeNodeCustomData[],
parentType?: ViewVariableType,
) => {
if (!nodes || nodes.length === 0) {
return getDefaultValueByType(parentType || type);
}
if (isArray && !parentType) {
const firstChild = nodes[0];
if (!firstChild) {
return [];
}
// 如果是数组类型,根据第一个子元素的类型生成默认值
const result = {};
if (firstChild.children && firstChild.children.length > 0) {
result[firstChild.name] = processChildren(
firstChild.children,
firstChild.type,
);
} else {
result[firstChild.name] = getDefaultValueByType(firstChild.type);
}
return [result];
}
return nodes.reduce(
(acc, node) => {
if (!node.name) {
return acc;
}
if (node.children && node.children.length > 0) {
const value = processChildren(node.children, node.type);
acc[node.name] = isArrayType(node.type) ? [value] : value;
} else {
acc[node.name] = getDefaultValueByType(node.type);
}
return acc;
},
{} satisfies Record<string, unknown>,
);
};
// 生成最终的JSON结构
const result = {
[name]: processChildren(children),
};
return formatJson(JSON.stringify(result));
};

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type VariableChannel } from '@coze-arch/bot-api/memory';
import { type ViewVariableType } from '@/store/variable-groups/types';
import { useVariableGroupsStore } from '@/store/variable-groups/store';
import { type Variable } from '@/store';
import { type SchemaNode } from '../../../json-editor/service/convert-schema-service';
/**
* 将转换后的数据转换为Variable
* @param data 转换后的数据
* @param baseInfo 基础信息
* @param originalVariable 原始变量用于保持variableId
* @returns Variable[]
*/
export const exportVariableService = (
data: SchemaNode[],
baseInfo: {
groupId: string;
channel: VariableChannel;
},
originalVariable?: Variable,
): Variable[] => {
const store = useVariableGroupsStore.getState();
const convertNode = (
node: SchemaNode,
parentId = '',
originalNode?: Variable,
): Variable => {
// 使用store中的createVariable方法创建基础变量
const baseVariable = store.createVariable({
variableType: node.type as ViewVariableType,
groupId: baseInfo.groupId,
parentId,
channel: baseInfo.channel,
});
// 如果存在原始节点保持其variableId
if (originalNode) {
baseVariable.variableId = originalNode.variableId;
baseVariable.description = originalNode.description;
}
// 更新变量的基本信息
baseVariable.name = node.name;
baseVariable.defaultValue = node.defaultValue;
// 递归处理子节点,尝试匹配原始子节点
if (node.children?.length) {
baseVariable.children = node.children.map((child, index) => {
const originalChild = originalNode?.children?.[index];
return convertNode(child, baseVariable.variableId, originalChild);
});
}
return baseVariable;
};
const variables = data.map(node => convertNode(node, '', originalVariable));
// 使用store中的updateMeta方法更新meta信息
store.updateMeta({ variables });
return variables;
};

View File

@@ -0,0 +1,74 @@
/*
* 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 Variable, ViewVariableType } from '@/store';
import {
traverse,
type TraverseContext,
type TraverseHandler,
} from './traverse';
const isOutputValueContext = (context: TraverseContext): boolean => {
if (
typeof context.node.value !== 'object' ||
typeof context.node.value.type === 'undefined'
) {
return false;
} else {
return true;
}
};
const cutOffNameLength =
(length: number): TraverseHandler =>
(context: TraverseContext): void => {
if (!isOutputValueContext(context)) {
return;
}
if (context.node.value.name.length > length) {
context.node.value.name = context.node.value.name.slice(0, length);
}
};
const cutOffDepth =
(depth: number): TraverseHandler =>
(context: TraverseContext): void => {
if (
!isOutputValueContext(context) ||
context.node.value.level !== depth ||
![ViewVariableType.Object, ViewVariableType.ArrayObject].includes(
context.node.value.type,
)
) {
return;
}
context.deleteSelf();
};
export const cutOffInvalidData = (params: {
data: Variable[];
allowDepth: number;
allowNameLength: number;
maxVariableCount: number;
}): Variable[] => {
const { data, allowDepth, allowNameLength, maxVariableCount } = params;
const cutOffVariableCountData = data.slice(0, maxVariableCount);
return traverse<Variable[]>(cutOffVariableCountData, [
cutOffNameLength(allowNameLength),
cutOffDepth(allowDepth),
]);
};

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 { nanoid } from 'nanoid';
import type { TreeNodeCustomData } from '../../../type';
import { traverse, type TraverseContext } from './traverse';
/** 计算路径 */
const getTreePath = (context: TraverseContext): string => {
const parents = context
.getParents()
.filter(
node =>
typeof node.value === 'object' &&
typeof node.value.name !== 'undefined' &&
typeof node.value.type !== 'undefined',
);
return parents.map(node => node.value.name).join('/');
};
/** 新旧数据保留 key 防止变量系统引用失效 */
export const mergeData = (params: {
newData: TreeNodeCustomData;
oldData: TreeNodeCustomData;
}): TreeNodeCustomData => {
const { newData, oldData } = params;
// 计算旧数据中路径与key的映射
const treeDataPathKeyMap = new Map<
string,
{
key: string;
}
>();
traverse(oldData, context => {
if (
typeof context.node.value !== 'object' ||
typeof context.node.value.key === 'undefined' ||
typeof context.node.value.type === 'undefined'
) {
return;
}
const stringifyPath = getTreePath(context);
treeDataPathKeyMap.set(stringifyPath, {
key: context.node.value.key,
});
});
// 新数据复用旧数据的key失败则重新生成
const newDataWithKey = traverse(newData, context => {
if (
typeof context.node.value !== 'object' ||
typeof context.node.value.type === 'undefined'
) {
return;
}
const stringifyPath = getTreePath(context);
const { key } = treeDataPathKeyMap.get(stringifyPath) || {
key: nanoid(),
};
context.node.value.key = key;
});
return newDataWithKey;
};

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-namespace */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type TraverseValue = any;
export interface TraverseNode {
value: TraverseValue;
container?: TraverseValue;
parent?: TraverseNode;
key?: string;
index?: number;
}
export interface TraverseContext {
node: TraverseNode;
setValue: (value: TraverseValue) => void;
getParents: () => TraverseNode[];
getPath: () => Array<string | number>;
getStringifyPath: () => string;
deleteSelf: () => void;
}
export type TraverseHandler = (context: TraverseContext) => void;
/**
* 深度遍历对象,对每个值做处理
* @param value 遍历对象
* @param handle 处理函数
*/
export const traverse = <T extends TraverseValue = TraverseValue>(
value: T,
handler: TraverseHandler | TraverseHandler[],
): T => {
const traverseHandler: TraverseHandler = Array.isArray(handler)
? (context: TraverseContext) => {
handler.forEach(handlerFn => handlerFn(context));
}
: handler;
TraverseUtils.traverseNodes({ value }, traverseHandler);
return value;
};
namespace TraverseUtils {
/**
* 深度遍历对象,对每个值做处理
* @param node 遍历节点
* @param handle 处理函数
*/
export const traverseNodes = (
node: TraverseNode,
handle: TraverseHandler,
): void => {
const { value } = node;
if (!value) {
// 异常处理
return;
}
if (Object.prototype.toString.call(value) === '[object Object]') {
// 对象,遍历对象的每个属性
Object.entries(value).forEach(([key, item]) =>
traverseNodes(
{
value: item,
container: value,
key,
parent: node,
},
handle,
),
);
} else if (Array.isArray(value)) {
// 数组,遍历数组的每个元素
// 从数组的末尾开始遍历,这样即使中途移除了某个元素,也不会影响到未处理的元素的索引
for (let index = value.length - 1; index >= 0; index--) {
const item: string = value[index];
traverseNodes(
{
value: item,
container: value,
index,
parent: node,
},
handle,
);
}
}
const context: TraverseContext = createContext({ node });
handle(context);
};
const createContext = ({
node,
}: {
node: TraverseNode;
}): TraverseContext => ({
node,
setValue: (value: unknown) => setValue(node, value),
getParents: () => getParents(node),
getPath: () => getPath(node),
getStringifyPath: () => getStringifyPath(node),
deleteSelf: () => deleteSelf(node),
});
const setValue = (node: TraverseNode, value: unknown) => {
// 设置值函数
// 引用类型,需要借助父元素修改值
// 由于是递归遍历所以需要根据node来判断是给对象的哪个属性赋值还是给数组的哪个元素赋值
if (!value || !node) {
return;
}
node.value = value;
// 从上级作用域node中取出containerkeyindex
const { container, key, index } = node;
if (key && container) {
container[key] = value;
} else if (typeof index === 'number') {
container[index] = value;
}
};
const getParents = (node: TraverseNode): TraverseNode[] => {
const parents: TraverseNode[] = [];
let currentNode: TraverseNode | undefined = node;
while (currentNode) {
parents.unshift(currentNode);
currentNode = currentNode.parent;
}
return parents;
};
const getPath = (node: TraverseNode): Array<string | number> => {
const path: Array<string | number> = [];
const parents = getParents(node);
parents.forEach(parent => {
if (parent.key) {
path.unshift(parent.key);
} else if (parent.index) {
path.unshift(parent.index);
}
});
return path;
};
const getStringifyPath = (node: TraverseNode): string => {
const path = getPath(node);
return path.reduce((stringifyPath: string, pathItem: string | number) => {
if (typeof pathItem === 'string') {
const re = /\W/g;
if (re.test(pathItem)) {
// 包含特殊字符
return `${stringifyPath}["${pathItem}"]`;
}
return `${stringifyPath}.${pathItem}`;
} else {
return `${stringifyPath}[${pathItem}]`;
}
}, '');
};
const deleteSelf = (node: TraverseNode): void => {
const { container, key, index } = node;
if (key && container) {
delete container[key];
} else if (typeof index === 'number') {
container.splice(index, 1);
}
};
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
/** 每一级树缩进宽度 */
export const TreeIndentWidth = 30;
/** 树节点展开收起按钮宽度 */
export const TreeCollapseWidth = 24;
// 名称最长50字符
export const MAX_NAME_LENGTH = 50;
// 最大深度限制
export const MAX_LEVEL = 3;
// 最大变量数量限制
export const MAX_JSON_VARIABLE_COUNT = 1;
// 最大JSON长度限制30kb
export const MAX_JSON_LENGTH = 30 * 1024;

View File

@@ -0,0 +1,375 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
import { useParams } from 'react-router-dom';
import React, {
useCallback,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { useShallow } from 'zustand/react/shallow';
import { nanoid } from 'nanoid';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { type DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import { type TreeProps } from '@coze-arch/bot-semi/Tree';
import { type VariableChannel } from '@coze-arch/bot-api/memory';
import { IconCozPlus } from '@coze-arch/coze-design/icons';
import { IconButton, Toast, Tree, useFormApi } from '@coze-arch/coze-design';
import { traverse } from '@/utils/traverse';
import { useVariableGroupsStore, ViewVariableType } from '@/store';
import { VariableTreeContext } from '@/context/variable-tree-context';
import { flatVariableTreeData } from './utils';
import { type TreeNodeCustomData } from './type';
import { ChangeMode } from './components/custom-tree-node/constants';
import CustomTreeNode from './components/custom-tree-node';
export interface VariableTreeProps {
groupId: string;
value: Array<TreeNodeCustomData>;
treeProps?: TreeProps;
readonly?: boolean;
className?: string;
style?: React.CSSProperties;
showAddButton?: boolean;
/** 默认变量类型 */
defaultVariableType?: ViewVariableType;
defaultCollapse?: boolean;
children?: React.ReactNode;
maxLimit?: number;
hideHeaderKeys?: string[];
channel: VariableChannel;
validateExistKeyword?: boolean;
onChange?: (changeValue: TreeNodeCustomData) => void;
}
export interface VariableTreeRef {
validate: () => void;
}
function useExpandedKeys(keys: string[], defaultCollapse: boolean) {
const [expandedKeys, setExpandedKeys] = useState(defaultCollapse ? [] : keys);
const expandTreeNode = useCallback((key: string) => {
setExpandedKeys(prev => [...new Set([...prev, key])]);
}, []);
const collapseTreeNode = useCallback((key: string) => {
setExpandedKeys(prev => prev.filter(expandedKey => expandedKey !== key));
}, []);
return { expandedKeys, expandTreeNode, collapseTreeNode };
}
export function Index(
props: VariableTreeProps,
ref: React.Ref<VariableTreeRef>,
) {
const {
readonly = false,
treeProps,
className,
style,
value,
defaultVariableType = ViewVariableType.String,
defaultCollapse = false,
maxLimit,
groupId,
channel,
hideHeaderKeys,
validateExistKeyword = false,
onChange,
} = props;
const {
createVariable,
addRootVariable,
addChildVariable,
updateVariable,
deleteVariable,
findAndModifyVariable,
} = useVariableGroupsStore(
useShallow(state => ({
createVariable: state.createVariable,
addRootVariable: state.addRootVariable,
addChildVariable: state.addChildVariable,
updateVariable: state.updateVariable,
deleteVariable: state.deleteVariable,
findAndModifyVariable: state.findAndModifyVariable,
})),
);
const formApi = useFormApi();
const isValueEmpty = !value || value.length === 0;
const itemKeysWithChildren = useMemo(() => {
const keys: string[] = [];
traverse(value, item => {
if (item.children?.length > 0) {
keys.push(item.variableId);
}
});
return keys;
}, [value]);
const flatTreeData = useMemo(() => flatVariableTreeData(value), [value]);
const { expandedKeys, expandTreeNode, collapseTreeNode } = useExpandedKeys(
itemKeysWithChildren,
defaultCollapse,
);
const params = useParams<DynamicParams>();
useImperativeHandle(ref, () => ({
validate: () => formApi.validate(),
}));
const disableAdd = useMemo(() => {
if (maxLimit === undefined) {
return false;
}
return (value?.length ?? 0) >= maxLimit;
}, [value, maxLimit]);
const showAddButton = !readonly && !disableAdd;
const onAdd = () => {
const newVariable = createVariable({
groupId,
parentId: '',
variableType: defaultVariableType,
channel,
});
addRootVariable(newVariable);
onChange?.(newVariable);
sendTeaEvent(EVENT_NAMES.memory_click_front, {
project_id: params?.project_id || '',
resource_type: 'variable',
action: 'add',
source: 'app_detail_page',
source_detail: 'memory_manage',
});
};
// 树节点的 change 方法
const onTreeNodeChange = (mode: ChangeMode, param: TreeNodeCustomData) => {
const findResult = findAndModifyVariable(
groupId,
item => item.variableId === param.variableId,
);
if (!findResult) {
Toast.error(I18n.t('workflow_detail_node_output_parsingfailed'));
return;
}
switch (mode) {
case ChangeMode.Append: {
const { variableId: parentId, channel: parentChannel } = findResult;
const childVariable = createVariable({
groupId,
parentId,
variableType: defaultVariableType,
channel: parentChannel,
});
addChildVariable(childVariable);
// 当前节点下新增节点 展开当前节点
if (findResult?.variableId) {
expandTreeNode(findResult.variableId);
}
sendTeaEvent(EVENT_NAMES.memory_click_front, {
project_id: params?.project_id || '',
resource_type: 'variable',
action: 'add',
source: 'app_detail_page',
source_detail: 'memory_manage',
});
break;
}
case ChangeMode.Update: {
updateVariable(param);
sendTeaEvent(EVENT_NAMES.memory_click_front, {
project_id: params?.project_id || '',
resource_type: 'variable',
action: 'edit',
source: 'app_detail_page',
source_detail: 'memory_manage',
});
break;
}
case ChangeMode.Delete: {
deleteVariable(param);
sendTeaEvent(EVENT_NAMES.memory_click_front, {
project_id: params?.project_id || '',
resource_type: 'variable',
action: 'delete',
source: 'app_detail_page',
source_detail: 'memory_manage',
});
break;
}
case ChangeMode.UpdateEnabled: {
findResult.enabled = param.enabled;
// 一键关闭所有子节点
traverse<TreeNodeCustomData>(findResult, node => {
if (!param.enabled) {
node.enabled = param.enabled;
}
});
// 子点开启,父节点也开启
if (findResult.parentId && findResult.enabled) {
const parentData = findAndModifyVariable(
groupId,
item => item.variableId === findResult.parentId,
);
if (parentData) {
parentData.enabled = findResult.enabled;
updateVariable(parentData);
}
}
updateVariable(findResult);
sendTeaEvent(EVENT_NAMES.memory_click_front, {
project_id: params?.project_id || '',
resource_type: 'variable',
action: param.enabled ? 'turn_on' : 'turn_off',
source: 'app_detail_page',
source_detail: 'memory_manage',
});
break;
}
case ChangeMode.Replace: {
updateVariable(param);
expandTreeNode(findResult.variableId);
sendTeaEvent(EVENT_NAMES.memory_click_front, {
project_id: params?.project_id || '',
resource_type: 'variable',
action: 'edit',
source: 'app_detail_page',
source_detail: 'memory_manage',
});
break;
}
default:
}
onChange?.(param);
};
if (readonly && isValueEmpty) {
return null;
}
return (
<VariableTreeContext.Provider value={{ groupId, variables: flatTreeData }}>
<div
className={classNames(
// 基础容器样式
'relative h-full',
// 交互状态
!readonly && 'cursor-default',
// 自定义类名
className,
)}
style={style}
>
<Tree
style={readonly ? {} : { overflow: 'inherit' }}
motion={false}
keyMaps={{
key: 'variableId',
}}
disabled={readonly}
className={classNames(
// 基础滚动行为
'overflow-x-auto',
// Tree 列表基础样式
[
// 列表容器样式
'[&_.semi-tree-option-list]:overflow-visible',
'[&_.semi-tree-option-list]:p-0',
'[&_.semi-tree-option-list>div:first-child]:mt-0',
// 选项样式
'[&_.semi-tree-option]:!pl-2',
].join(' '),
// 交互状态样式
readonly
? '[&_.semi-tree-option-list-block_.semi-tree-option:hover]:bg-inherit'
: [
'[&_.semi-tree-option-list-block_.semi-tree-option:hover]:bg-transparent',
'[&_.semi-tree-option-list-block_.semi-tree-option:active]:bg-transparent',
].join(' '),
)}
renderFullLabel={renderFullLabelProps => {
const { data } = renderFullLabelProps;
const currentLevelReadOnly = readonly || data.IsReadOnly;
const onCollapse = (collapsed: boolean) => {
const { variableId } = renderFullLabelProps.data;
if (!variableId) {
return;
}
if (collapsed) {
expandTreeNode(variableId);
} else {
collapseTreeNode(variableId);
}
};
return (
<CustomTreeNode
{...renderFullLabelProps}
hideHeaderKeys={hideHeaderKeys}
validateExistKeyword={validateExistKeyword}
onChange={onTreeNodeChange}
hasObjectLike={data.meta.hasObjectLike}
readonly={currentLevelReadOnly}
couldCollapse={(data.children?.length ?? 0) > 0}
collapsed={renderFullLabelProps.expandStatus.expanded}
onCollapse={onCollapse}
/>
);
}}
emptyContent={<></>}
expandedKeys={[...expandedKeys, nanoid()]}
treeData={value}
{...treeProps}
/>
{showAddButton ? (
<div className="flex items-center my-3">
<IconButton icon={<IconCozPlus />} onClick={onAdd}>
{I18n.t('workflow_detail_node_output_add_subitem')}
</IconButton>
</div>
) : null}
</div>
</VariableTreeContext.Provider>
);
}
// 导出可调用ref方法的组件
export const VariableTree = React.forwardRef(Index);

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 CSSProperties } from 'react';
import { type Variable, type ViewVariableType } from '@/store';
import { type ChangeMode } from './components/custom-tree-node/constants';
export interface RecursedParamDefinition {
name?: string;
/** Tree 组件要求每一个节点都有 key而 key 不适合用名称(前后缀)等任何方式赋值,最终确定由接口转换层一次性提供随机 key */
fieldRandomKey?: string;
desc?: string;
type: ViewVariableType;
children?: RecursedParamDefinition[];
}
export type TreeNodeCustomData = Variable;
export interface CustomTreeNodeFuncRef {
data: TreeNodeCustomData;
level: number;
readonly: boolean;
// 通用change方法
onChange: (mode: ChangeMode, param: TreeNodeCustomData) => void;
// 定制的类型改变的change方法主要用于自定义render使用
// 添加子项
onAppend: () => void;
// 删除该项
onDelete: () => void;
// 类型改变时内部的调用方法主要用于从类Object类型转为其他类型时需要删除所有子项
onSelectChange: (
val?: string | number | Array<unknown> | Record<string, unknown>,
) => void;
}
export type WithCustomStyle<T = object> = {
className?: string;
style?: CSSProperties;
} & T;

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { traverse } from '@/utils/traverse';
import { type Variable, type VariableGroup } from '@/store';
import { type TreeNodeCustomData } from './type';
interface RootFindResult {
isRoot: true;
data: TreeNodeCustomData;
parentData: null;
}
interface ChildrenFindResult {
isRoot: false;
parentData: TreeNodeCustomData;
data: TreeNodeCustomData;
}
export type FindDataResult = RootFindResult | ChildrenFindResult | null;
/**
* 根据target数组找到key在该项的值和位置主要是获取位置方便操作parent的children
*/
export function findCustomTreeNodeDataResult(
target: Array<TreeNodeCustomData>,
variableId: string,
): FindDataResult {
const dataInRoot = target.find(item => item.variableId === variableId);
if (dataInRoot) {
// 如果是根节点
return {
isRoot: true,
parentData: null,
data: dataInRoot,
};
}
function findDataInChildrenLoop(
customChildren: Array<TreeNodeCustomData>,
parentData?: TreeNodeCustomData,
): FindDataResult {
function findDataLoop(
customData: TreeNodeCustomData,
_parentData: TreeNodeCustomData,
): FindDataResult {
if (customData.variableId === variableId) {
return {
isRoot: false,
parentData: _parentData,
data: customData,
};
}
if (customData.children && customData.children.length > 0) {
return findDataInChildrenLoop(
customData.children as Array<TreeNodeCustomData>,
customData,
);
}
return null;
}
for (const child of customChildren) {
const childResult = findDataLoop(child, parentData || child);
if (childResult) {
return childResult;
}
}
return null;
}
return findDataInChildrenLoop(target);
}
// 将groupVariableMeta打平为viewVariableTreeNode[]
export function flatGroupVariableMeta(
groupVariableMeta: VariableGroup[],
maxDepth = Infinity,
) {
const res: Variable[] = [];
traverse(
groupVariableMeta,
item => {
res.push(...item.varInfoList);
},
'subGroupList',
maxDepth,
);
return res;
}
export const flatVariableTreeData = (treeData: Variable[]) => {
const res: Variable[] = [];
traverse(
treeData,
item => {
res.push(item);
},
'children',
);
return res;
};