feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 ?? ''} />
|
||||
);
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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: '',
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />,
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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),
|
||||
]);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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中取出container,key,index
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user