feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
const { defineConfig } = require('@coze-arch/stylelint-config');
module.exports = defineConfig({
extends: [],
});

View File

@@ -0,0 +1,16 @@
# @coze-agent-ide/space-bot-publish-to-base
> Project template for react component with storybook.
## Features
- [x] eslint & ts
- [x] esm bundle
- [x] umd bundle
- [x] storybook
## Commands
- init: `rush update`
- dev: `npm run dev`
- build: `npm run build`

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View File

@@ -0,0 +1,98 @@
{
"name": "@coze-agent-ide/space-bot-publish-to-base",
"version": "0.0.1",
"description": "我受够了繁文缛节",
"license": "Apache-2.0",
"author": "wanglitong@bytedance.com",
"maintainers": [],
"main": "src/index.ts",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@blueprintjs/core": "^5.1.5",
"@coze-agent-ide/bot-editor-context-store": "workspace:*",
"@coze-agent-ide/bot-input-length-limit": "workspace:*",
"@coze-agent-ide/chat-background": "workspace:*",
"@coze-agent-ide/tool": "workspace:*",
"@coze-agent-ide/tool-config": "workspace:*",
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-error": "workspace:*",
"@coze-arch/bot-flags": "workspace:*",
"@coze-arch/bot-hooks": "workspace:*",
"@coze-arch/bot-http": "workspace:*",
"@coze-arch/bot-icons": "workspace:*",
"@coze-arch/bot-md-box-adapter": "workspace:*",
"@coze-arch/bot-semi": "workspace:*",
"@coze-arch/bot-space-api": "workspace:*",
"@coze-arch/bot-studio-store": "workspace:*",
"@coze-arch/bot-tea": "workspace:*",
"@coze-arch/bot-utils": "workspace:*",
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
"@coze-arch/fetch-stream": "workspace:*",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-arch/report-events": "workspace:*",
"@coze-arch/report-tti": "workspace:*",
"@coze-common/assets": "workspace:*",
"@coze-common/chat-area": "workspace:*",
"@coze-data/database-creator": "workspace:*",
"@coze-data/e2e": "workspace:*",
"@coze-data/knowledge-modal-base": "workspace:*",
"@coze-data/knowledge-resource-processor-core": "workspace:*",
"@coze-data/reporter": "workspace:*",
"@coze-data/utils": "workspace:*",
"@coze-foundation/global-store": "workspace:*",
"@coze-studio/bot-detail-store": "workspace:*",
"@coze-studio/components": "workspace:*",
"@coze-studio/user-store": "workspace:*",
"@douyinfe/semi-icons": "^2.36.0",
"@douyinfe/semi-illustrations": "^2.36.0",
"ahooks": "^3.7.8",
"classnames": "^2.3.2",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.7",
"immer": "^10.0.3",
"lodash-es": "^4.17.21",
"nanoid": "^4.0.2",
"qs": "^6.11.2",
"rc-slider": "10.6.2",
"react-hotkeys-hook": "~4.5.0",
"react-markdown": "^8.0.3",
"zustand": "^4.4.7"
},
"devDependencies": {
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/stylelint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@types/ajv": "~1.0.0",
"@types/draft-js": "^0.11.12",
"@types/json-schema": "~7.0.15",
"@types/lodash-es": "^4.17.10",
"@types/node": "^18",
"@types/papaparse": "^5.3.9",
"@types/qs": "^6.9.7",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@types/react-resizable": "^3.0.6",
"@vitest/coverage-v8": "~3.0.5",
"debug": "^4.3.4",
"devcert": "1.2.2",
"stylelint": "^15.11.0",
"tailwindcss": "~3.3.3",
"utility-types": "^3.10.0",
"vitest": "~3.0.5"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
}

View File

@@ -0,0 +1,34 @@
.error_checkbox {
:global {
.semi-checkbox-inner-display {
/* stylelint-disable-next-line declaration-no-important */
border: solid 1px var(--coz-stroke-hglt-red) !important;
}
}
}
.large {
:global {
.semi-icon {
display: flex;
align-items: center;
justify-content: center;
/* stylelint-disable-next-line declaration-no-important */
width: 24px !important;
/* stylelint-disable-next-line declaration-no-important */
height: 24px !important;
/* stylelint-disable-next-line declaration-no-important */
font-size: 22px !important;
}
.semi-checkbox-inner,
.semi-checkbox-inner-display {
/* stylelint-disable-next-line declaration-no-important */
width: 24px !important;
/* stylelint-disable-next-line declaration-no-important */
height: 24px !important;
border-radius: 8px;
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC } from 'react';
import classnames from 'classnames';
import { Checkbox } from '@coze-arch/coze-design';
import styles from './index.module.less';
type CheckboxProps = Parameters<typeof Checkbox>[0] & {
isError?: boolean;
size?: 'large';
};
export const BigCheckbox: FC<CheckboxProps> = ({
isError,
children,
className,
size = 'large',
...rest
}) => (
<Checkbox
className={classnames(
className,
isError && styles.error_checkbox,
size === 'large' && styles.large,
)}
{...rest}
>
{children}
</Checkbox>
);

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const MARKDOWN_TOOLTIP_WIDTH = 340;
export const MARKDOWN_TOOLTIP_CONTENT_MAX_WIDTH = 300;
export const ERROR_LINE_HEIGHT = 14;
export const INPUT_CONFIG_TEXT_MAX_CHAR = 2000;

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createContext, useContext } from 'react';
import { type FeishuBaseConfigFe } from '../types';
import { type ConfigStore } from '../store';
export const StoreContext = createContext<{
store?: ConfigStore;
}>({});
export const useConfigStoreRaw = () => useContext(StoreContext).store;
export const useConfigStoreGuarded = () => {
const store = useConfigStoreRaw();
if (!store) {
throw new Error('impossible store unprovided');
}
return store;
};
export const useConfigAsserted = (): FeishuBaseConfigFe => {
const useStore = useConfigStoreGuarded();
const config = useStore(state => state.config);
if (!config) {
throw new Error('cannot get config');
}
return config;
};

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, type ReactNode } from 'react';
import { I18n } from '@coze-arch/i18n';
import {
IllustrationConstruction,
IllustrationFailure,
} from '@douyinfe/semi-illustrations';
export const ExceptionDisplay: FC<{ title: string; image: ReactNode }> = ({
title,
image,
}) => (
<div className="flex flex-col gap-[16px] justify-center items-center h-[80%]">
{image}
<span className="coz-fg-plus text-[16px] font-medium leading-[22px]">
{title}
</span>
</div>
);
/**
* 加载失败
*/
export const LoadFailedDisplay = () => (
<ExceptionDisplay
image={<IllustrationFailure className="h-[140px] w-[140px]" />}
title={I18n.t('plugin_exception')}
/>
);
/**
* 无数据
*/
export const NoDataDisplay = () => (
<ExceptionDisplay
image={<IllustrationConstruction className="h-[140px] w-[140px]" />}
title={I18n.t('debug_asyn_task_notask')}
/>
);

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FC, useState } from 'react';
import { I18n } from '@coze-arch/i18n';
import { InputComponentType } from '@coze-arch/bot-api/connector_api';
import { type InputConfigFe } from '../types';
import {
useConfigAsserted,
useConfigStoreGuarded,
} from '../context/store-context';
import { ERROR_LINE_HEIGHT } from '../constants';
import { type HeaderItem, SortableFieldTable } from './sortable-field-table';
import {
BaseInputFieldLine,
inputFieldColumnWidth,
type InputComponentOption,
InputLineCommonContext,
} from './field-line/input-config-line';
export const BaseInputFieldsTable: FC = () => {
const config = useConfigAsserted();
const inputFields = config.input_config || [];
const updateConfigByImmer = useConfigStoreGuarded()(
state => state.updateConfigByImmer,
);
const [errorLines, setErrorLines] = useState<string[]>([]);
return (
<div className="mt-[4px]">
<InputLineCommonContext.Provider
value={{
inputFieldsSelectorList: config.input_type_list,
onChange: val => {
updateConfigByImmer(cfg => {
const fields = cfg.input_config;
const idx = fields.findIndex(field => field._id === val._id);
fields[idx] = val;
});
},
inputOptions: getInputOptions(),
onToggleError: (id, error) => {
setErrorLines(lines => {
if (!error) {
return lines.filter(lineId => lineId !== id);
}
const inLines = lines.includes(id);
if (!inLines) {
return [...lines, id];
}
return lines;
});
},
}}
>
<SortableFieldTable<InputConfigFe>
enabled={inputFields.length > 1}
headers={getInputFieldsHeaders()}
data={inputFields.map(field => ({
deletable: field.invalid ?? false,
bizComponent: BaseInputFieldLine,
data: field,
getKey: data => data._id,
onDelete: delItem => {
updateConfigByImmer(cfg => {
cfg.input_config = cfg.input_config.filter(
e => e._id !== delItem._id,
);
});
},
deleteButtonStyle: {
width: 32,
},
lineStyle: {
marginTop: 8,
paddingBottom: errorLines.some(id => id.includes(field._id))
? ERROR_LINE_HEIGHT
: 0,
},
}))}
getId={data => data.data._id}
onChange={mix =>
updateConfigByImmer(cfg => {
cfg.input_config = mix.map(x => x.data);
})
}
/>
</InputLineCommonContext.Provider>
</div>
);
};
const getInputFieldsHeaders = (): HeaderItem[] => [
{
name: I18n.t('publish_base_configFields_field'),
required: false,
width: inputFieldColumnWidth.field,
},
{
name: I18n.t('publish_base_configFields_title'),
required: true,
width: inputFieldColumnWidth.title,
},
{
name: I18n.t('publish_base_configFields_placeholder'),
required: false,
width: inputFieldColumnWidth.placeholder,
},
{
name: I18n.t('publish_base_configFields_component'),
required: true,
width: inputFieldColumnWidth.control,
},
{
name: I18n.t('required'),
required: false,
width: inputFieldColumnWidth.required,
},
];
const getInputOptions = (): InputComponentOption[] => [
{
label: I18n.t('publish_base_inputFieldConfig_textInput'),
value: InputComponentType.Text,
},
{
label: I18n.t('publish_base_inputFieldConfig_singleSelect'),
value: InputComponentType.SingleSelect,
},
{
label: I18n.t('publish_base_inputFieldConfig_multiSelect'),
value: InputComponentType.MultiSelect,
},
{
label: I18n.t('publish_base_inputFieldConfig_fieldSelector'),
value: InputComponentType.FieldSelector,
},
];

View File

@@ -0,0 +1,365 @@
/*
* 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 ReactMarkdown from 'react-markdown';
import {
type FC,
type PropsWithChildren,
useEffect,
useRef,
useState,
} from 'react';
import { nanoid } from 'nanoid';
import classNames from 'classnames';
import { useMutationObserver } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import { IconCozPlus } from '@coze-arch/coze-design/icons';
import { Button, Select } from '@coze-arch/coze-design';
import {
type OutputTypeInfo,
OutputSubComponentType,
} from '@coze-arch/bot-api/connector_api';
import { getIsStructOutput } from '../validate/utils';
import {
validateOutputStructGroupByKey,
validateOutputStructPrimaryKey,
} from '../validate';
import {
type BaseOutputStructLineType,
type FeishuBaseConfigFe,
type OutputSubComponentFe,
} from '../types';
import { default as mdStyles } from '../md-tooltip/index.module.less';
import { useConfigStoreGuarded } from '../context/store-context';
import { ERROR_LINE_HEIGHT } from '../constants';
import { type HeaderItem, SortableFieldTable } from './sortable-field-table';
import { FormSubtitle } from './form-title';
import { useRequireVerifyCenter } from './field-line/require-verify-center';
import {
BaseOutputStructLine,
OutputLineCommonContext,
outputStructColumnWidth,
type OutputStructVerifyRes,
} from './field-line/output-struct-line';
import styles from './index.module.less';
export const BaseOutputFieldsTable: FC<{
config: FeishuBaseConfigFe;
}> = ({ config }) => {
const outputTypeId = config.output_type;
const updateConfigByImmer = useConfigStoreGuarded()(
state => state.updateConfigByImmer,
);
const outputTypeTips = getOutputInfo(
config.output_type_list,
outputTypeId,
)?.tips;
return (
<div className="mt-[6px]">
<Select
style={{
width: '100%',
}}
defaultValue={config.output_type}
optionList={config.output_type_list.map(info => ({
label: info.name,
value: info.id,
}))}
onChange={val => {
updateConfigByImmer(cfg => {
const type = Number(val);
cfg.output_type = type;
if (getIsStructOutput(type)) {
cfg.output_sub_component.type = OutputSubComponentType.Object;
const itemList = cfg.output_sub_component.item_list;
if (!itemList?.length) {
cfg.output_sub_component.item_list = [
getDefaultStructFieldItem(),
];
}
} else {
cfg.output_sub_component.type = OutputSubComponentType.None;
}
});
}}
/>
{getIsStructOutput(outputTypeId) ? (
<OutputStructConfig config={config} />
) : null}
{outputTypeTips ? (
<div
className={classNames(
'rounded-[8px] px-[8px] py-[12px] coz-mg-hglt mt-[8px]',
'text-[12px] leading-[16px] coz-fg-primary',
)}
style={{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- .
// @ts-expect-error
'--tooltip-content-max-width': '580px',
}}
>
<ReactMarkdown className={mdStyles.md_wrap}>
{outputTypeTips}
</ReactMarkdown>
</div>
) : null}
</div>
);
};
const getOutputInfo = (
infoList: OutputTypeInfo[],
id: number,
): OutputTypeInfo | undefined => infoList.find(info => info.id === id);
const getDefaultStructFieldItem = (): BaseOutputStructLineType => ({
key: '',
output_type: undefined,
_id: nanoid(),
});
const OutputStructConfig: FC<{
config: FeishuBaseConfigFe;
}> = ({ config }) => {
const structFields = config.output_sub_component.item_list;
const updateConfigByImmer = useConfigStoreGuarded()(
state => state.updateConfigByImmer,
);
const [requiredToCheck, setRequiredToCheck] = useState(false);
const { registerVerifyFn } = useRequireVerifyCenter();
const [errorLines, setErrorLinesRaw] = useState<string[]>([]);
const setErrorLines = (hasError: boolean, id: string) => {
setErrorLinesRaw(lines => {
if (!hasError) {
return lines.filter(lineId => lineId !== id);
}
const inLines = lines.includes(id);
if (!inLines) {
return [...lines, id];
}
return lines;
});
};
useEffect(() => {
const unregister = registerVerifyFn(() => setRequiredToCheck(true));
return unregister;
}, []);
return (
<>
<div className="ml-[8px] mt-[16px]">
<FormSubtitle
title={I18n.t('publish_base_config_structOutputConfig')}
required
tooltip={config.output_sub_component.struct_output_desc}
suffix={
<Button
icon={<IconCozPlus />}
onClick={() => {
updateConfigByImmer(cfg => {
const newList = cfg.output_sub_component.item_list || [];
newList.push(getDefaultStructFieldItem());
cfg.output_sub_component.item_list = newList;
});
}}
color="secondary"
size="small"
className="ml-auto"
>
{I18n.t('Add_1')}
</Button>
}
/>
</div>
<OutputLineCommonContext.Provider
value={{
onChange: val => {
updateConfigByImmer(cfg => {
const fields = cfg.output_sub_component.item_list;
if (!fields) {
return;
}
const idx =
fields?.findIndex(field => field._id === val._id) ?? -1;
if (idx < 0) {
return;
}
fields[idx] = val;
});
},
list: config.object_value_type_list,
getShowRequireWarn: curLine => {
const res = getShowRequireWarnImpl({
curLine,
allFields: config.output_sub_component?.item_list || [],
requiredToCheck,
});
return res;
},
onToggleError: (id, error) => {
setErrorLines(error, id);
},
}}
>
<SortableFieldTable<BaseOutputStructLineType>
linesWrapper={OutputConfigLinesWrapper}
enabled={(structFields?.length || 0) > 1}
headers={getBaseInfoHeaders(config.output_sub_component)}
onChange={mix =>
updateConfigByImmer(cfg => {
cfg.output_sub_component.item_list = mix.map(m => m.data);
})
}
getId={mix => mix.data._id}
style={{
marginTop: 6,
}}
data={(structFields || []).map(field => ({
bizComponent: BaseOutputStructLine,
deletable: (structFields || []).length > 1,
data: field,
getKey: data => data._id,
onDelete: delItem => {
updateConfigByImmer(cfg => {
const list = cfg.output_sub_component.item_list;
if (!list) {
return;
}
cfg.output_sub_component.item_list = list.filter(
item => item._id !== delItem._id,
);
});
},
lineStyle: {
paddingBottom: errorLines.some(id => id.includes(field._id))
? ERROR_LINE_HEIGHT
: 0,
},
deleteButtonStyle: {
width: 24,
minWidth: 0,
height: 24,
padding: 0,
},
}))}
/>
</OutputLineCommonContext.Provider>
</>
);
};
const OutputConfigLinesWrapper: FC<PropsWithChildren> = ({ children }) => {
const wrapRef = useRef<HTMLDivElement | null>(null);
const [withScrollbar, setWithScrollbar] = useState(false);
const detectScrollbar = () => {
if (!wrapRef.current) {
return;
}
const isVerticalScrollbar =
wrapRef.current.scrollHeight > wrapRef.current.clientHeight;
setWithScrollbar(isVerticalScrollbar);
};
useEffect(detectScrollbar, []);
useMutationObserver(detectScrollbar, wrapRef, {
childList: true,
});
return (
<div
ref={wrapRef}
className={classNames(
'overflow-x-hidden',
!withScrollbar && 'pr-[8px]',
styles.output_config,
)}
>
{children}
</div>
);
};
const getBaseInfoHeaders = (
outputComponent: OutputSubComponentFe,
): HeaderItem[] => [
{
name: I18n.t('publish_base_configFields_key'),
required: true,
width: outputStructColumnWidth.key,
},
{
name: I18n.t('publish_base_configStruct_dataType'),
required: true,
width: outputStructColumnWidth.outputType,
},
{
name: I18n.t('publish_base_configStruct_id'),
required: true,
width: outputStructColumnWidth.groupByKey,
tooltip: outputComponent.struct_id_desc,
},
{
name: I18n.t('publish_base_configStruct_primary'),
required: true,
width: outputStructColumnWidth.primary,
tooltip: outputComponent.struct_primary_desc,
style: I18n.language.includes('zh')
? {}
: {
fontSize: 12,
lineHeight: '16px',
display: 'inline-block',
},
},
];
const getShowRequireWarnImpl = ({
curLine,
allFields,
requiredToCheck,
}: {
curLine: BaseOutputStructLineType;
allFields: BaseOutputStructLineType[];
requiredToCheck: boolean;
}): OutputStructVerifyRes => {
const idx = allFields.findIndex(line => line._id === curLine._id);
const res: OutputStructVerifyRes = {
groupByKey: {
warn: false,
},
primary: {
warn: false,
},
};
if (!requiredToCheck || idx > 0) {
return res;
}
const groupByKeyVerifyRes = validateOutputStructGroupByKey(allFields);
const primaryKeyVerifyRes = validateOutputStructPrimaryKey(allFields);
if (!groupByKeyVerifyRes.ok) {
res.groupByKey.tip = groupByKeyVerifyRes.error;
res.groupByKey.warn = true;
}
if (!primaryKeyVerifyRes.ok) {
res.primary.tip = primaryKeyVerifyRes.error;
res.primary.warn = true;
}
return res;
};

View File

@@ -0,0 +1,5 @@
.input_deletable {
:global(.semi-input-suffix) {
margin: 0;
}
}

View File

@@ -0,0 +1,161 @@
/*
* 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 { createContext, type FC, useContext } from 'react';
import { nanoid } from 'nanoid';
import { produce } from 'immer';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozPlus, IconCozTrashCan } from '@coze-arch/coze-design/icons';
import { Button, Input } from '@coze-arch/coze-design';
import { SortableFieldTable } from '../sortable-field-table';
import type { InputComponentSelectOption, InputConfigFe } from '../../types';
import styles from './index.module.less';
const SelectSubEditContext = createContext<{
onChange?: (choice: InputComponentSelectOption) => void;
onDelete?: (data: InputComponentSelectOption) => void;
choiceLength?: number;
}>({});
export const SelectSubEditComponent: FC<{
config: InputConfigFe;
onUpdate: (cfg: InputConfigFe) => void;
}> = ({ config, onUpdate }) => (
<div>
<div className="flex ml-[8px] mt-[20px]">
<span className="coz-fg-secondary text-[12px] font-medium leading-[16px]">
{I18n.t('publish_base_inputFieldConfig_options')}
</span>
<Button
icon={<IconCozPlus />}
onClick={() => {
onUpdate(
produce<InputConfigFe>(cfg => {
cfg.input_component.choice.push({
name: '',
id: nanoid(),
});
})(config),
);
}}
color="secondary"
size="small"
className="ml-auto"
>
{I18n.t('Add_1')}
</Button>
</div>
<SelectSubEditContext.Provider
value={{
onChange: choiceItem => {
onUpdate(
produce<InputConfigFe>(cfg => {
const { choice } = cfg.input_component;
const idx = choice.findIndex(i => i.id === choiceItem.id);
choice.splice(idx, 1, choiceItem);
})(config),
);
},
choiceLength: config.input_component.choice?.length || 0,
onDelete: delData => {
onUpdate(
produce<InputConfigFe>(cfg => {
cfg.input_component.choice = cfg.input_component.choice.filter(
it => it.id !== delData.id,
);
})(config),
);
},
}}
>
<SortableFieldTable<InputComponentSelectOption>
enabled={config.input_component.choice.length > 1}
headless
style={{
padding: 0,
}}
headers={[
{
width: 192,
name: '',
required: false,
},
]}
data={config.input_component.choice.map(data => ({
data,
deletable: false,
lineStyle: {
paddingRight: 0,
paddingTop: 8,
},
getKey: it => it.id,
bizComponent: SelectEditLine,
}))}
getId={data => data.data.id}
onChange={data => {
const choice = data.map(it => it.data);
onUpdate(
produce<InputConfigFe>(cfg => {
cfg.input_component.choice = choice;
})(config),
);
}}
/>
</SelectSubEditContext.Provider>
</div>
);
const SelectEditLine: FC<{
data: InputComponentSelectOption;
}> = ({ data }) => {
const { onChange, onDelete, choiceLength } = useContext(SelectSubEditContext);
if (choiceLength === undefined || !onChange || !onDelete) {
throw new Error('impossible context member miss');
}
return (
<Input
value={data.name}
onChange={str => {
onChange({
id: data.id,
name: str,
});
}}
className={classNames(styles.input_deletable, 'w-full mr-[8px]')}
suffix={
choiceLength <= 1 ? null : (
<Button
color="secondary"
onClick={() => onDelete(data)}
icon={<IconCozTrashCan />}
style={{
width: 24,
height: 24,
minWidth: 0,
padding: 0,
}}
></Button>
)
}
/>
);
};

View File

@@ -0,0 +1,386 @@
/*
* 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 { createContext, type FC, useContext, useState } from 'react';
import { nanoid } from 'nanoid';
import { produce } from 'immer';
import classnames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import {
Button,
Input,
InputNumber,
Popover,
Radio,
RadioGroup,
Select,
Tag,
Typography,
} from '@coze-arch/coze-design';
import {
InputComponentType,
type InputTypeInfo,
} from '@coze-arch/bot-api/connector_api';
import { getIsSelectType } from '../../validate/utils';
import { validateSingleInputFieldControl } from '../../validate';
import { type InputConfigFe } from '../../types';
import { type ConfigStoreState } from '../../store';
import { MdTooltip } from '../../md-tooltip';
import { useConfigStoreGuarded } from '../../context/store-context';
import { INPUT_CONFIG_TEXT_MAX_CHAR } from '../../constants';
import { BigCheckbox } from '../../big-checkbox';
import { useRequireVerify } from './use-require-verify';
import { RequiredWarn } from './required-warn';
import { SelectSubEditComponent } from './input-config-line-select-edit';
export const inputFieldColumnWidth = {
field: 110,
title: 118,
placeholder: 118,
control: 118,
required: 60,
};
export interface InputComponentOption {
label: string;
value: InputComponentType;
}
const USER_QUERY_FIELD_NAME = 'user_query';
const INVALID_LINE_OPACITY = 0.3;
export const InputLineCommonContext = createContext<{
onChange?: (val: InputConfigFe) => void;
inputFieldsSelectorList?: InputTypeInfo[];
inputOptions?: InputComponentOption[];
onToggleError?: (id: string, error: boolean) => void;
}>({});
const getInputConfig = (storeState: ConfigStoreState, id: string) => {
const data = storeState.config?.input_config.find(item => item._id === id);
if (!data) {
throw new Error(`cannot find data of ${id}`);
}
return data;
};
export const BaseInputFieldLine: FC<{
data: InputConfigFe;
// eslint-disable-next-line @coze-arch/max-line-per-function -- 鼠鼠我呀,也很无奈
}> = ({ data: { _id: id } }) => {
const { onChange, inputFieldsSelectorList, inputOptions, onToggleError } =
useContext(InputLineCommonContext);
const store = useConfigStoreGuarded();
const data = useConfigStoreGuarded()(state => getInputConfig(state, id));
if (!data) {
throw new Error(`cannot find data of ${id}`);
}
if (
!inputFieldsSelectorList ||
!onChange ||
!inputOptions ||
!onToggleError
) {
throw new Error('impossible context member miss');
}
const changeByImmer = (updater: (sth: InputConfigFe) => void) => {
onChange(produce<InputConfigFe>(updater)(data));
};
const isUserQuery = data.field === USER_QUERY_FIELD_NAME;
const [showPopover, setShowPopover] = useState(false);
const getVal = () => getInputConfig(store.getState(), id);
const titleRequire = useRequireVerify({
getVal,
verify: config => !!config?.title,
onChange: isError => onToggleError(`${id}#title`, isError),
});
const controlRequire = useRequireVerify({
getVal,
verify: config => !!config?.input_component.type,
onChange: isError => onToggleError(`${id}#control`, isError),
});
return (
<>
<div
className="coz-fg-secondary text-[14px] leading-[20px] flex items-center"
style={{
width: inputFieldColumnWidth.field,
opacity: data.invalid ? INVALID_LINE_OPACITY : 1,
}}
>
<Typography.Text
className={classnames('mr-[3px] coz-fg-secondary')}
ellipsis={{ showTooltip: true }}
>
{data.field}
</Typography.Text>
{data.desc ? (
<MdTooltip content={data.desc} tooltipPosition="right">
<Button
className="!w-[14px] !h-[16px] !min-w-0 !p-0"
theme="borderless"
type="secondary"
color="secondary"
icon={<IconCozInfoCircle className="text-[12px]" />}
/>
</MdTooltip>
) : null}
{data.invalid ? (
<Tag color="primary" size="mini">
{I18n.t('publish_base_configFields_invalid')}
</Tag>
) : null}
</div>
<div
style={{
width: inputFieldColumnWidth.title,
position: 'relative',
}}
>
<Input
error={titleRequire.showWarn}
onBlur={titleRequire.onTrigger}
value={data.title}
onChange={val =>
changeByImmer(origin => {
origin.title = val;
})
}
placeholder={I18n.t('publish_base_configFields_title_placeholder')}
disabled={data.invalid}
maxLength={30}
/>
{titleRequire.showWarn ? <RequiredWarn /> : null}
</div>
<Input
value={data.placeholder}
onChange={val =>
changeByImmer(origin => {
origin.placeholder = val;
})
}
placeholder={I18n.t(
'publish_base_configFields_placeholder_placeholder',
)}
style={{
width: inputFieldColumnWidth.placeholder,
}}
disabled={data.invalid}
maxLength={30}
/>
<Popover
visible={showPopover}
trigger="custom"
position="top"
content={
<InputFieldControlConfig
originConfig={data}
inputOptions={inputOptions}
closePanel={() => {
setShowPopover(false);
controlRequire.onTrigger();
}}
onUpdate={(config: InputConfigFe) => {
onChange(config);
}}
inputFieldsSelectorList={inputFieldsSelectorList}
/>
}
>
<div
style={{
width: inputFieldColumnWidth.control,
position: 'relative',
}}
>
<div
onClick={() => {
if (!data.invalid) {
setShowPopover(true);
}
}}
>
<Select
disabled={data.invalid}
optionList={inputOptions}
value={data.input_component.type}
className="w-full"
dropdownStyle={{
display: 'none',
}}
renderOptionItem={() => null}
placeholder={I18n.t(
'publish_base_configFields_component_placeholder',
)}
hasError={controlRequire.showWarn}
/>
</div>
{controlRequire.showWarn ? <RequiredWarn /> : null}
</div>
</Popover>
{data.invalid ? null : (
<BigCheckbox
style={{
marginLeft: 'auto',
}}
checked={isUserQuery || data.required}
disabled={isUserQuery}
onChange={e => {
const val = Boolean(e.target.checked);
changeByImmer(cur => {
cur.required = val;
});
}}
/>
)}
</>
);
};
const InputFieldControlConfig: FC<{
onUpdate: (config: InputConfigFe) => void;
inputOptions: InputComponentOption[];
inputFieldsSelectorList: InputTypeInfo[];
originConfig: InputConfigFe;
closePanel: () => void;
}> = ({
inputOptions,
onUpdate: submitSubConfig,
originConfig,
closePanel,
inputFieldsSelectorList,
}) => {
const [config, setConfig] = useState(() => originConfig);
const fieldsSelectorOptions = inputFieldsSelectorList.map(opt => ({
value: opt.id,
label: opt.name,
}));
return (
<div className="pl-[12px] pb-[16px]">
<div className="overflow-y-auto max-h-[320px] pt-[12px] pr-[12px]">
<div className="ml-[8px]">
<p className="coz-fg-secondary text-[12px] font-medium leading-[16px]">
{I18n.t('publish_base_configFields_component')}
</p>
</div>
<RadioGroup
className="mx-[8px] mt-[14px] grid grid-cols-2 gap-[12px]"
defaultValue={config.input_component.type}
onChange={val => {
const type = val.target.value as InputComponentType;
setConfig(
produce<InputConfigFe>(curConfig => {
curConfig.input_component.type = type;
if (
getIsSelectType(type) &&
!curConfig.input_component.choice?.length
) {
curConfig.input_component.choice.push({
name: '',
id: nanoid(),
});
}
}),
);
}}
>
{inputOptions.map(option => (
<Radio key={option.value} value={option.value} className="">
{option.label}
</Radio>
))}
</RadioGroup>
{config.input_component.type === InputComponentType.Text ? (
<div className="ml-[8px] mt-[20px]">
<div className="coz-fg-secondary text-[12px] font-medium leading-[16px]">
{I18n.t('publish_base_inputFieldConfig_maxChars')}
</div>
<InputNumber
style={{
marginTop: 6,
}}
defaultValue={config.input_component.max_char}
max={INPUT_CONFIG_TEXT_MAX_CHAR}
min={config.required ? 1 : 0}
onChange={val => {
setConfig(
produce<InputConfigFe>(curConfig => {
curConfig.input_component.max_char = Number(val);
}),
);
}}
/>
</div>
) : null}
{getIsSelectType(config.input_component.type) ? (
<SelectSubEditComponent config={config} onUpdate={setConfig} />
) : null}
{config.input_component.type === InputComponentType.FieldSelector ? (
<div>
<div className="flex ml-[8px] mt-[20px]">
<span className="coz-fg-secondary text-[12px] font-medium leading-[16px]">
{I18n.t('publish_base_inputFieldConfig_supports')}
</span>
</div>
<Select
style={{
width: 256,
marginTop: 6,
}}
optionList={fieldsSelectorOptions}
multiple
defaultValue={config.input_component.supported_type}
maxTagCount={2}
expandRestTagsOnClick
onChange={valRaw => {
const val = valRaw as number[];
setConfig(
produce<InputConfigFe>(curConfig => {
curConfig.input_component.supported_type = val;
}),
);
}}
></Select>
</div>
) : null}
</div>
<div className="flex gap-[8px] items-center mt-[24px] mr-[12px]">
<Button color="primary" onClick={closePanel} className="ml-auto">
{I18n.t('Cancel')}
</Button>
<Button
disabled={!validateSingleInputFieldControl(config)}
onClick={() => {
submitSubConfig(config);
closePanel();
}}
>
{I18n.t('Confirm')}
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,234 @@
/*
* 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, createContext, useContext, useEffect } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Input, Select } from '@coze-arch/coze-design';
import { type OutputTypeInfo } from '@coze-arch/bot-api/connector_api';
import { getIsNumberOutput, getIsTextOutput } from '../../validate/utils';
import { type BaseOutputStructLineType } from '../../types';
import type { ConfigStoreState } from '../../store';
import { useConfigStoreGuarded } from '../../context/store-context';
import { BigCheckbox } from '../../big-checkbox';
import { useRequireVerify } from './use-require-verify';
import { RequiredWarn } from './required-warn';
export const OutputLineCommonContext = createContext<{
onChange?: (val: BaseOutputStructLineType) => void;
list?: OutputTypeInfo[];
getShowRequireWarn?: (val: BaseOutputStructLineType) => OutputStructVerifyRes;
onToggleError?: (id: string, error: boolean) => void;
}>({});
export interface OutputStructVerifyRes {
groupByKey: {
warn: boolean;
tip?: string;
};
primary: {
warn: boolean;
tip?: string;
};
}
const getOutputFieldConfig = (storeState: ConfigStoreState, id: string) => {
const data = storeState.config?.output_sub_component.item_list?.find(
item => item._id === id,
);
if (!data) {
throw new Error(`cannot find data of ${id}`);
}
return data;
};
export const BaseOutputStructLine: FC<{
data: BaseOutputStructLineType;
// eslint-disable-next-line @coze-arch/max-line-per-function -- /
}> = ({ data: { _id: id } }) => {
const { list, onChange, getShowRequireWarn, onToggleError } = useContext(
OutputLineCommonContext,
);
const store = useConfigStoreGuarded();
const data = useConfigStoreGuarded()(state =>
getOutputFieldConfig(state, id),
);
if (!data) {
throw new Error(`cannot find data of ${id}`);
}
if (!list || !onChange || !getShowRequireWarn || !onToggleError) {
throw new Error('impossible context member miss');
}
const { groupByKey: groupByKeyRequire, primary: primaryRequire } =
getShowRequireWarn(data);
const getVal = () => getOutputFieldConfig(store.getState(), id);
const keyRequire = useRequireVerify({
getVal,
verify: config => !!config?.key,
onChange: isError => onToggleError(`${id}#key`, isError),
});
const typeRequire = useRequireVerify({
getVal,
verify: config => Number.isInteger(config.output_type),
onChange: isError => onToggleError(`${id}$type`, isError),
});
useEffect(() => {
const hasError = groupByKeyRequire.warn || primaryRequire.warn;
onToggleError(data._id, hasError);
}, [groupByKeyRequire.warn, primaryRequire.warn]);
return (
<>
<div
style={{
width: outputStructColumnWidth.key,
margin: '6px 0',
position: 'relative',
}}
>
<Input
error={keyRequire.showWarn}
value={data.key}
onBlur={keyRequire.onTrigger}
placeholder={I18n.t('publish_base_configFields_key_placeholder')}
onChange={val => {
onChange({
...data,
key: val,
});
}}
/>
{keyRequire.showWarn ? <RequiredWarn /> : null}
</div>
<div
style={{
width: outputStructColumnWidth.outputType,
position: 'relative',
}}
>
<Select
defaultValue={data.output_type}
optionList={list.map(info => ({
value: info.id,
label: info.name,
}))}
placeholder={I18n.t('publish_base_configFields_dataType_placeholder')}
onBlur={typeRequire.onTrigger}
onChange={val => {
onChange({
...data,
output_type: Number(val),
});
typeRequire.onTrigger();
}}
hasError={typeRequire.showWarn}
style={{
width: '100%',
}}
/>
{typeRequire.showWarn ? <RequiredWarn /> : null}
</div>
<div
style={{
width: outputStructColumnWidth.groupByKey,
position: 'relative',
}}
>
<BigCheckbox
checked={data.is_group_by_key}
/**
* is_group_by_key: 只允许提交 text 类型
* 可以切换场景:
* 1. 已经勾选:任意类型
* 2. 未勾选:仅 text 类型
*/
disabled={
!(data.is_group_by_key || getIsTextOutput(data.output_type))
}
isError={groupByKeyRequire.warn}
onChange={e => {
const val = Boolean(e.target.checked);
onChange({
...data,
is_group_by_key: val,
});
}}
/>
{groupByKeyRequire.warn ? (
<RequiredWarn
text={groupByKeyRequire.tip}
style={{
marginLeft: 0,
}}
/>
) : null}
</div>
<div
style={{
width: outputStructColumnWidth.primary,
position: 'relative',
}}
>
<BigCheckbox
checked={data.is_primary}
isError={primaryRequire.warn}
/**
* is_primary: 只允许提交 text 或 number 类型
* 可以切换场景:
* 1. 已经勾选:任意类型
* 2. 未勾选:仅 text 与 number 类型
*/
disabled={
!(
data.is_primary ||
getIsNumberOutput(data.output_type) ||
getIsTextOutput(data.output_type)
)
}
onChange={e => {
const val = Boolean(e.target.checked);
onChange({
...data,
is_primary: val,
});
}}
/>
{primaryRequire.warn ? (
<RequiredWarn
text={primaryRequire.tip}
style={{
marginLeft: 0,
}}
/>
) : null}
</div>
</>
);
};
const FIRST_TWO_COLUMN_TRANSFER_SPACE = 30;
// 总和为 566滚动条留 8左侧拖拽按钮 16gap 8 * 4 删除按钮 24
// (566 - 8 - 16 - 4 * 8 - 24 - (44 + 96)) / 2 = 173
export const outputStructColumnWidth = {
key: 173 + FIRST_TWO_COLUMN_TRANSFER_SPACE,
outputType: 173 - FIRST_TWO_COLUMN_TRANSFER_SPACE,
groupByKey: 44,
// 给国际化预留一些宽度
primary: 96,
};

View File

@@ -0,0 +1,68 @@
/*
* 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 {
createContext,
type FC,
type PropsWithChildren,
useContext,
useRef,
} from 'react';
interface FieldsRequireCenter {
verifyFns: Set<() => void>;
triggerAllVerify: () => void;
registerVerifyFn: (fn: () => void) => () => void;
}
const FieldsRequireCenterContext = createContext<FieldsRequireCenter>({
verifyFns: new Set(),
triggerAllVerify: () => undefined,
registerVerifyFn: () => () => undefined,
});
export const FieldsRequireCenterWrapper: FC<PropsWithChildren> = ({
children,
}) => {
const fns = useRef(new Set<() => void>());
return (
<FieldsRequireCenterContext.Provider
value={{
verifyFns: fns.current,
triggerAllVerify: () => {
fns.current.forEach(fn => fn());
},
registerVerifyFn: fn => {
fns.current.add(fn);
return () => {
fns.current.delete(fn);
};
},
}}
>
{children}
</FieldsRequireCenterContext.Provider>
);
};
FieldsRequireCenterWrapper.displayName = 'FieldsRequireCenterWrapper';
export const useRequireVerifyCenter = (): Omit<
FieldsRequireCenter,
'verifyFns'
> => useContext(FieldsRequireCenterContext);

View File

@@ -0,0 +1,51 @@
/*
* 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, type FC } from 'react';
import { merge } from 'lodash-es';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { ERROR_LINE_HEIGHT } from '../../constants';
export const RequiredWarn: FC<{
text?: string;
className?: string;
style?: CSSProperties;
absolute?: boolean;
}> = props => {
const { text, style, className, absolute = true } = props;
return (
<div
className={classNames(
className,
'coz-fg-hglt-red text-[10px]',
'ml-[8px]',
'whitespace-nowrap',
absolute ? 'absolute' : '',
)}
style={merge(
{
lineHeight: `${ERROR_LINE_HEIGHT}px`,
},
style,
)}
>
{text || I18n.t('publish_base_configFields_requiredWarn')}
</div>
);
};

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useState } from 'react';
import { useRequireVerifyCenter } from './require-verify-center';
export const useRequireVerify = <T>({
getVal,
verify,
onChange,
}: {
getVal: () => T;
verify: (val: T) => boolean;
onChange?: (isError: boolean) => void;
}) => {
const [showWarn, setShowWarn] = useState(false);
const { registerVerifyFn } = useRequireVerifyCenter();
const onTrigger = () => {
const val = getVal();
const verified = verify(val);
const isError = !verified;
setShowWarn(isError);
onChange?.(isError);
};
useEffect(() => {
const unregister = registerVerifyFn(onTrigger);
return unregister;
}, []);
return {
showWarn,
onTrigger,
};
};

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type CSSProperties, type FC, type ReactNode } from 'react';
import classNames from 'classnames';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tooltip } from '@coze-arch/coze-design';
import { MdTooltip } from '../md-tooltip';
export const FormTitle: FC<{
title: string;
tooltip?: string;
style?: CSSProperties;
required?: boolean;
}> = ({ title, style, tooltip, required }) => (
<p
style={style}
className={classNames(
'text-[16px]',
'coz-fg-plus',
'font-medium',
'leading-[22px]',
'flex',
'items-center',
)}
>
<span>{title}</span>
{required ? (
<i className="coz-fg-hglt-red text-[12px] font-medium">*</i>
) : null}
{tooltip ? (
<Tooltip content={tooltip}>
<span className="cursor-pointer ml-[4px] h-[22px] flex items-center">
<IconCozInfoCircle className="text-[14px] coz-fg-secondary" />
</span>
</Tooltip>
) : null}
</p>
);
export const FormSubtitle: FC<{
title: string;
required: boolean;
tooltip?: string;
style?: CSSProperties;
suffix?: ReactNode;
}> = ({ title, required, tooltip, style, suffix }) => (
<p
className={classNames('flex', 'justify-start', 'items-center')}
style={style}
>
<span className="text-[12px] coz-fg-secondary leading-[16px] font-medium">
{title}
</span>
{required ? (
<i className="coz-fg-hglt-red text-[12px] font-medium">*</i>
) : null}
<MdTooltip content={tooltip}>
<span className="cursor-pointer ml-[4px] h-[16px] flex items-center">
<IconCozInfoCircle className="text-[12px] coz-fg-secondary" />
</span>
</MdTooltip>
{suffix}
</p>
);

View File

@@ -0,0 +1,10 @@
.output_config {
&::-webkit-scrollbar-thumb {
background: var(--coz-fg-dim);
border-radius: 3px;
}
&::-webkit-scrollbar {
width: 7px;
}
}

View File

@@ -0,0 +1,403 @@
/*
* 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 ReactMarkdown from 'react-markdown';
import {
forwardRef,
useImperativeHandle,
useRef,
useState,
memo,
type FC,
} from 'react';
import { nanoid } from 'nanoid';
import { cloneDeep, omit } from 'lodash-es';
import { useRequest } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import {
IconCozCross,
IconCozLongArrowUp,
} from '@coze-arch/coze-design/icons';
import {
Button,
IconButton,
Modal,
Spin,
Tag,
Toast,
} from '@coze-arch/coze-design';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { type PublishConnectorInfo } from '@coze-arch/bot-api/developer_api';
import {
type FeishuBaseConfig,
type InputComponent,
type InputConfig,
type OutputSubComponent,
type OutputSubComponentItem,
} from '@coze-arch/bot-api/connector_api';
import { connectorApi, DeveloperApi } from '@coze-arch/bot-api';
import { OUTPUT_TYPE_TEXT } from '../validate/utils';
import { useSubscribeAndUpdateConfig } from '../validate/field-interaction';
import { validateFullConfig } from '../validate';
import {
type FeishuBaseConfigFe,
type InputComponentFe,
type InputConfigFe,
type OutputSubComponentFe,
type SaveConfigPayload,
} from '../types';
import { type ConfigStore, createConfigStore } from '../store';
import { LoadFailedDisplay } from '../expection-display';
import { StoreContext, useConfigAsserted } from '../context/store-context';
import { StepIndicator } from './step-indicator';
import { FormSubtitle, FormTitle } from './form-title';
import {
FieldsRequireCenterWrapper,
useRequireVerifyCenter,
} from './field-line/require-verify-center';
import { BaseOutputFieldsTable } from './base-output-fields-table';
import { BaseInputFieldsTable } from './base-input-fields-table';
export const JumpButton: FC<{
url: string;
completed: boolean;
}> = ({ url, completed }) => (
<Button
color="secondary"
onClick={() => {
window.open(url);
}}
icon={<IconCozLongArrowUp className="rotate-45" />}
iconPosition="right"
size="small"
className={!completed ? '!coz-fg-hglt' : ''}
>
{!completed
? I18n.t('publish_base_configFields_complete_Information_fill_out')
: I18n.t('publish_base_configFields_complete_Information_edit')}
</Button>
);
export const FeishuBaseModal = memo(
forwardRef<
{
openModal: () => void;
},
{
botId: string;
record: PublishConnectorInfo;
onSaved: (id: string) => void;
}
>(({ botId, record, onSaved }, ref) => {
const [showModal, setShowModal] = useState(false);
useImperativeHandle(ref, () => ({
openModal: () => {
setShowModal(true);
run();
},
}));
const storeRef = useRef<ConfigStore | null>(null);
const formRef = useRef<{
configFormSubmit: () => void;
} | null>(null);
if (!storeRef.current) {
storeRef.current = createConfigStore();
}
useSubscribeAndUpdateConfig(storeRef.current);
const { data, loading, run, mutate, cancel, error } = useRequest(
async () => {
const { config } = await connectorApi.GetFeishuBaseConfig({
bot_id: botId,
});
if (!config) {
return undefined;
}
return convertBaseConfig(config);
},
{
manual: true,
onSuccess: res => {
if (!res) {
return;
}
storeRef.current?.getState().setConfig(res);
},
},
);
const hideModalAndClearData = () => {
setShowModal(false);
cancel();
mutate();
};
return (
<Modal
visible={showModal}
onCancel={hideModalAndClearData}
closeOnEsc={false}
maskClosable={false}
footer={
<Button
color="hgltplus"
size="default"
onClick={() => {
formRef.current?.configFormSubmit();
}}
>
{I18n.t('Confirm')}
</Button>
}
size="large"
linearGradientMask
header={
<div className="flex items-center justify-between h-[40px]">
<span className="text-[20px] font-medium leading-[28px] coz-fg-primary">
{I18n.t('publish_base_config_configFeishuBase')}
</span>
<IconButton
onClick={hideModalAndClearData}
icon={<IconCozCross className="text-[18px]" />}
className="w-[40px] !h-[40px] -pr-2"
color="secondary"
/>
</div>
}
>
<Spin spinning={loading}>
{data?.description ? (
<ReactMarkdown
linkTarget="_blank"
className="coz-fg-secondary text-[14px] leading-[20px]"
>
{data.description}
</ReactMarkdown>
) : null}
{data ? (
<StoreContext.Provider value={{ store: storeRef.current }}>
<FieldsRequireCenterWrapper>
<ConfigForm
ref={formRef}
record={record}
botId={botId}
onSaved={() => {
onSaved(record.id);
setShowModal(false);
}}
/>
</FieldsRequireCenterWrapper>
</StoreContext.Provider>
) : (
<div className="h-[60px]" />
)}
{!data && error ? <LoadFailedDisplay /> : null}
</Spin>
</Modal>
);
}),
);
const ConfigForm = forwardRef<
{ configFormSubmit: () => void },
{
botId: string;
record: PublishConnectorInfo;
onSaved: () => void;
}
>(({ botId, record, onSaved }, ref) => {
const config = useConfigAsserted();
const { input_desc, output_desc, to_complete_info } = config;
const { url = '', completed = false } = to_complete_info ?? {};
const couldSubmit = validateFullConfig(config);
const { triggerAllVerify } = useRequireVerifyCenter();
useImperativeHandle(ref, () => ({
configFormSubmit: () => {
triggerAllVerify();
if (!couldSubmit) {
Toast.error({
content: I18n.t('publish_base_configFields_ unfinished_toast'),
});
return;
}
submitConfig();
},
}));
const { run: submitConfig } = useRequest(
() => {
const spaceId = useSpaceStore.getState().getSpaceId();
return DeveloperApi.BindConnector({
space_id: spaceId,
bot_id: botId,
connector_id: record.id,
connector_info: {
config: JSON.stringify(getSubmitPayload(config)),
},
});
},
{
manual: true,
onSuccess: () => {
Toast.success({
content: I18n.t('Save_success'),
});
onSaved();
},
},
);
return (
<div className="mt-[28px] pb-[32px]">
<div>
<div className="flex items-center gap-2">
<StepIndicator number={1} />
<FormTitle title={I18n.t('publish_base_config_configBaseInfo')} />
</div>
<FormSubtitle
required
title={I18n.t('publish_base_config_configOutputType')}
tooltip={output_desc}
style={{
marginTop: 9,
}}
/>
</div>
<BaseOutputFieldsTable config={config} />
<div className="mt-[32px]">
<div className="flex items-center gap-2">
<StepIndicator number={2} />
<FormTitle
title={I18n.t('publish_base_configFields')}
tooltip={input_desc}
/>
</div>
</div>
<BaseInputFieldsTable />
{to_complete_info ? (
<>
<div className="flex items-center gap-2 mt-6">
<StepIndicator number={3} />
<FormTitle
title={I18n.t(
'publish_base_configFields_complete_Information_title',
)}
required
/>
{completed ? (
<>
<Tag color="green">
{I18n.t('publish_base_configFields_status_completed')}
</Tag>
<JumpButton completed={completed} url={url} />
</>
) : null}
</div>
{!completed ? (
<div className="mt-[6px] flex items-center">
{I18n.t(
'publish_base_configFields_complete_Information_describe',
)}
<JumpButton completed={completed} url={url} />
</div>
) : null}
</>
) : null}
</div>
);
});
const getSubmitPayload = (config: FeishuBaseConfigFe): SaveConfigPayload => {
const res: SaveConfigPayload = cloneDeep({
output_type: config.output_type,
input_config: config.input_config.map(cfg => {
const inputConfig: InputConfig = {
...cfg,
input_component: reverseInputComponent(cfg.input_component),
};
return omit(inputConfig, '_id');
}),
output_sub_component: reverseOutputSubComponent(
config.output_sub_component,
),
});
res.output_sub_component.item_list = (
res.output_sub_component.item_list || []
).map(cfg => omit(cfg, '_id'));
return res;
};
const reverseOutputSubComponent = (
output: OutputSubComponentFe,
): OutputSubComponent => ({
...output,
item_list: (output.item_list || []).map(item => {
const res: OutputSubComponentItem = {
...omit(item, '_id'),
output_type: item.output_type ?? OUTPUT_TYPE_TEXT,
};
return res;
}),
});
const convertInputComponent = (cfg: InputComponent): InputComponentFe => {
const { choice } = cfg;
const res: InputComponentFe = {
...cfg,
choice: (choice || []).map(c => ({
name: c,
id: nanoid(),
})),
};
return res;
};
const reverseInputComponent = (cfg: InputComponentFe): InputComponent => {
const { choice } = cfg;
const res: InputComponent = {
...cfg,
choice: choice.map(c => c.name),
};
return res;
};
const convertBaseConfig = (config: FeishuBaseConfig): FeishuBaseConfigFe => {
const configFe: FeishuBaseConfigFe = {
...config,
output_sub_component: {
...config.output_sub_component,
item_list: (config.output_sub_component.item_list || []).map(item => ({
...item,
_id: nanoid(),
})),
},
input_config: config.input_config?.map(cfg => {
const res: InputConfigFe = {
...cfg,
input_component: convertInputComponent(cfg.input_component),
_id: nanoid(),
};
return res;
}),
};
return configFe;
};

View File

@@ -0,0 +1,240 @@
/*
* 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 ComponentType,
type CSSProperties,
type FC,
forwardRef,
type JSX,
type PropsWithChildren,
type ReactElement,
type ReactNode,
useEffect,
useMemo,
useRef,
} from 'react';
import classNames from 'classnames';
import { SortableList } from '@coze-studio/components/sortable-list';
import { type ITemRenderProps, type ConnectDnd } from '@coze-studio/components';
import { IconCozHandle, IconCozTrashCan } from '@coze-arch/coze-design/icons';
import { Button } from '@coze-arch/coze-design';
import { MdTooltip } from '../md-tooltip';
export interface HeaderItem {
name: string;
required: boolean;
width: number;
tooltip?: string;
style?: CSSProperties;
}
export interface SortableFieldTableMethod {
addRow: () => boolean;
}
export interface IData<Data extends object> {
data: Data;
deletable: boolean;
getKey: (data: Data) => string;
onDelete?: (data: Data) => void;
bizComponent: ComponentType<{ data: Data }>;
lineStyle?: CSSProperties;
deleteButtonStyle?: CSSProperties;
}
interface SortableFieldTableProps<Data extends object> {
className?: string;
headers: HeaderItem[];
data: IData<Data>[];
getId: (data: IData<Data>) => string;
onChange: (data: IData<Data>[]) => void;
headless?: boolean;
style?: CSSProperties;
enabled: boolean;
linesWrapper?: ComponentType;
}
const DefaultLinesWrapper: FC<PropsWithChildren> = ({ children }) => (
<>{children}</>
);
export const SortableFieldTable = <T extends object>({
className,
headers,
data,
getId,
onChange,
headless,
enabled,
style,
linesWrapper,
}: SortableFieldTableProps<T>): ReactElement => {
const uniqueSymbol = useMemo(() => Symbol(), []);
const LinesWrapper = linesWrapper || DefaultLinesWrapper;
return (
<div
className={classNames(
headless ? '' : 'coz-bg-primary',
headless ? '' : 'coz-stroke-primary border-solid border-[1px]',
'px-[12px] pt-[12px] pb-[12px]',
'rounded-[8px]',
className,
)}
style={style}
>
{headless ? null : <FieldTableHeader headers={headers} />}
<LinesWrapper>
<SortableList
enabled={enabled}
getId={getId}
list={data}
onChange={onChange}
itemRender={ItemRender as never}
type={uniqueSymbol}
/>
</LinesWrapper>
</div>
);
};
const ItemRender = <Data extends object>(
props: ITemRenderProps<IData<Data>>,
): JSX.Element => {
const { data: bizProps } = props;
const BizComponent = bizProps.bizComponent;
return (
<FieldSortLine
gap={8}
connect={props.connect}
deletable={bizProps.deletable}
onDelete={() => bizProps.onDelete?.(bizProps.data)}
style={bizProps.lineStyle}
deleteButtonStyle={bizProps.deleteButtonStyle}
>
<BizComponent key={bizProps.getKey(bizProps.data)} data={bizProps.data} />
</FieldSortLine>
);
};
const TableFieldLine = forwardRef<
HTMLDivElement,
PropsWithChildren<{
className?: string;
gap?: number;
prefix?: ReactNode;
style?: CSSProperties;
}>
>(({ children, className, gap, prefix, style }, ref) => (
<div
className={classNames(className, 'flex items-center')}
ref={ref}
style={style}
>
{prefix}
<div
className="flex items-center w-full"
style={{
gap,
}}
>
{children}
</div>
</div>
));
TableFieldLine.displayName = 'TableFieldLine';
const FieldSortLine: FC<
PropsWithChildren<{
deletable?: boolean;
connect: ConnectDnd;
gap?: number;
style?: CSSProperties;
deleteButtonStyle?: CSSProperties;
onDelete: () => void;
}>
> = ({
children,
deletable,
connect,
gap,
style,
onDelete,
deleteButtonStyle,
}) => {
const dropRef = useRef<HTMLDivElement | null>(null);
const dragRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
connect(dropRef, dragRef);
}, []);
return (
<TableFieldLine
style={style}
gap={gap}
ref={dropRef}
prefix={
<div className="cursor-grab h-full mr-[4px] w-[12px]" ref={dragRef}>
<IconCozHandle className="text-[12px]" />
</div>
}
>
{children}
{deletable ? (
<Button
color="secondary"
onClick={onDelete}
style={deleteButtonStyle}
icon={<IconCozTrashCan />}
></Button>
) : null}
</TableFieldLine>
);
};
const FieldTableHeader: FC<{ headers: HeaderItem[] }> = ({ headers }) => (
<TableFieldLine
className="border-0 border-b-[1px] coz-stroke-primary border-solid h-[28px] mb-[12px]"
gap={8}
prefix={
<div
style={{
minWidth: 16,
}}
/>
}
>
{headers.map(header => (
<div
key={header.name}
className={classNames(
'text-[14px] coz-fg-secondary font-medium leading-[20px]',
'inline-flex items-center',
)}
style={{
width: header.width,
...header.style,
}}
>
{header.name}
{header.required ? <i className="coz-fg-hglt-red">*</i> : null}
<MdTooltip content={header.tooltip} />
</div>
))}
</TableFieldLine>
);

View File

@@ -0,0 +1,44 @@
/*
* 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, type FC } from 'react';
import classNames from 'classnames';
export const StepIndicator: FC<{
number: number;
className?: string;
style?: CSSProperties;
}> = ({ number, className, style }) => (
<div
style={style}
className={classNames(
className,
'coz-mg-hglt',
'w-[20px]',
'h-[20px]',
'coz-fg-hglt',
'text-[14px]',
'font-medium',
'flex',
'items-center',
'justify-center',
'rounded-[50%]',
)}
>
{number}
</div>
);

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { FeishuBaseModal } from './feishu-base-modal';
export {
ExceptionDisplay,
LoadFailedDisplay,
NoDataDisplay,
} from './expection-display';

View File

@@ -0,0 +1,11 @@
.md_wrap {
max-width: var(--tooltip-content-max-width);
:global {
p,
img,
table {
max-width: var(--tooltip-content-max-width);
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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 ReactMarkdown from 'react-markdown';
import { type FC, type ReactNode } from 'react';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tooltip } from '@coze-arch/coze-design';
import {
MARKDOWN_TOOLTIP_CONTENT_MAX_WIDTH,
MARKDOWN_TOOLTIP_WIDTH,
} from '../constants';
import styles from './index.module.less';
export const MdTooltip: FC<{
content?: string;
children?: ReactNode;
tooltipPosition?: Parameters<typeof Tooltip>[0]['position'];
}> = ({ content, children, tooltipPosition }) => {
if (!content) {
return null;
}
return (
<Tooltip
content={
<ReactMarkdown className={styles.md_wrap}>{content}</ReactMarkdown>
}
position={tooltipPosition}
style={{
maxWidth: MARKDOWN_TOOLTIP_WIDTH,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- css var
// @ts-expect-error
'--tooltip-content-max-width': `${MARKDOWN_TOOLTIP_CONTENT_MAX_WIDTH}px`,
}}
>
{children || (
<span className="cursor-pointer ml-[2px] h-[16px] w-[16px] inline-flex items-center">
<IconCozInfoCircle className="text-[12px] coz-fg-secondary" />
</span>
)}
</Tooltip>
);
};

View File

@@ -0,0 +1,51 @@
/*
* 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 { create } from 'zustand';
import { produce } from 'immer';
import { type FeishuBaseConfigFe } from '../types';
export interface ConfigStoreState {
config: FeishuBaseConfigFe | null;
}
export interface ConfigStoreAction {
setConfig: (cfg: FeishuBaseConfigFe) => void;
updateConfigByImmer: (mutateFn: (cur: FeishuBaseConfigFe) => void) => void;
clear: () => void;
}
const getDefaultState = (): ConfigStoreState => ({
config: null,
});
export const createConfigStore = () =>
create<ConfigStoreState & ConfigStoreAction>((set, get) => ({
...getDefaultState(),
setConfig: cfg => set({ config: cfg }),
updateConfigByImmer: updater => {
const { config } = get();
if (!config) {
return;
}
const newConfig = produce<FeishuBaseConfigFe>(updater)(config);
set({ config: newConfig });
},
clear: () => set(getDefaultState()),
}));
export type ConfigStore = ReturnType<typeof createConfigStore>;

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
type FeishuBaseConfig,
type InputComponent,
type InputConfig,
type OutputSubComponent,
type OutputSubComponentItem,
} from '@coze-arch/bot-api/connector_api';
export type OutputSubComponentItemFe = Omit<
OutputSubComponentItem,
'output_type'
> & {
output_type: number | undefined;
};
export type BaseOutputStructLineType = OutputSubComponentItemFe & {
// eslint-disable-next-line @typescript-eslint/naming-convention -- frontend private usage
_id: string;
};
export type OutputSubComponentFe = Omit<OutputSubComponent, 'item_list'> & {
item_list?: BaseOutputStructLineType[];
};
export type FeishuBaseConfigFe = Omit<
FeishuBaseConfig,
'output_sub_component' | 'input_config'
> & {
output_sub_component: OutputSubComponentFe;
input_config: InputConfigFe[];
};
export interface InputComponentSelectOption {
name: string;
id: string;
}
export type InputComponentFe = Omit<InputComponent, 'choice'> & {
choice: InputComponentSelectOption[];
};
export type InputConfigFe = Omit<InputConfig, 'input_component'> & {
// eslint-disable-next-line @typescript-eslint/naming-convention -- .
_id: string;
input_component: InputComponentFe;
};
export type SaveConfigPayload = Pick<
FeishuBaseConfig,
'output_type' | 'output_sub_component' | 'input_config'
>;

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect } from 'react';
import { isEqual } from 'lodash-es';
import { produce } from 'immer';
import { getIsStructOutput } from '../utils';
import type { FeishuBaseConfigFe } from '../../types';
import { type ConfigStore } from '../../store';
import { mutateOutputStruct } from './output-struct';
export const useSubscribeAndUpdateConfig = (store: ConfigStore) => {
useEffect(() => {
const unsub = store.subscribe((state, prevState) => {
const curConfig = state.config;
const preConfig = prevState.config;
const updatedConfig = produce<FeishuBaseConfigFe | null>(cfg =>
mutateFieldsInteraction(cfg, preConfig),
)(curConfig);
if (!updatedConfig || isEqual(curConfig, updatedConfig)) {
return;
}
state.setConfig(updatedConfig);
});
return unsub;
}, []);
};
const mutateFieldsInteraction = (
config: FeishuBaseConfigFe | null,
preConfig: FeishuBaseConfigFe | null,
) => {
if (!config) {
return;
}
if (!getIsStructOutput(config.output_type)) {
return;
}
if (isEqual(config, preConfig)) {
return;
}
mutateOutputStruct(
config.output_sub_component,
preConfig?.output_sub_component,
);
};

View File

@@ -0,0 +1,160 @@
/*
* 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 {
verifyOutputStructFieldAsGroupByKey,
verifyOutputStructFieldAsPrimaryKey,
} from '../utils';
import type {
BaseOutputStructLineType,
OutputSubComponentFe,
} from '../../types';
export const mutateOutputStruct = (
outputStructConfig: OutputSubComponentFe,
preOutputStructConfig?: OutputSubComponentFe,
) => {
const fields = outputStructConfig.item_list;
if (!fields || !fields.length) {
return;
}
mutateOutputStructPrimaryKey(fields, preOutputStructConfig?.item_list);
mutateOutputStructGroupByKey(fields, preOutputStructConfig?.item_list);
};
const getIsPrimaryField = (field: BaseOutputStructLineType) => field.is_primary;
const getIsGroupByKeyField = (field: BaseOutputStructLineType) =>
field.is_group_by_key;
const mutateOutputStructGroupByKey = (
fields: BaseOutputStructLineType[],
preFields?: BaseOutputStructLineType[],
) => {
const groupByKeyFields = fields.filter(getIsGroupByKeyField);
if (!groupByKeyFields.length) {
return;
}
const getter = (field: BaseOutputStructLineType) => field.is_group_by_key;
const setter = (field: BaseOutputStructLineType, val: boolean) =>
(field.is_group_by_key = val);
mutatePositiveFieldsMoreThanOne({
curFields: groupByKeyFields,
getter,
setter,
matchFn: filed => !!filed.is_group_by_key,
preFields,
});
mutateOnlyPositiveField({
curFields: groupByKeyFields,
getter,
setter,
verify: verifyOutputStructFieldAsGroupByKey,
});
};
const mutateOutputStructPrimaryKey = (
fields: BaseOutputStructLineType[],
preFields?: BaseOutputStructLineType[],
) => {
const primaryFields = fields.filter(getIsPrimaryField);
if (!primaryFields.length) {
return;
}
const setter = (field: BaseOutputStructLineType, val: boolean) =>
(field.is_primary = val);
const getter = (field: BaseOutputStructLineType) => field.is_primary;
mutatePositiveFieldsMoreThanOne({
curFields: primaryFields,
getter,
setter,
matchFn: field => !!field.is_primary,
preFields,
});
mutateOnlyPositiveField({
curFields: primaryFields,
getter,
setter,
verify: verifyOutputStructFieldAsPrimaryKey,
});
};
const mutateOnlyPositiveField = ({
curFields,
getter,
setter,
verify,
}: {
curFields: BaseOutputStructLineType[];
getter: (field: BaseOutputStructLineType) => boolean | undefined;
setter: (field: BaseOutputStructLineType, val: boolean) => void;
verify: (field: BaseOutputStructLineType) => boolean;
}) => {
const onlyField = curFields.at(0);
if (!onlyField) {
return;
}
const notValid = !verify(onlyField);
if (notValid && getter(onlyField)) {
setter(onlyField, false);
}
};
const mutatePositiveFieldsMoreThanOne = ({
curFields,
matchFn,
setter,
getter,
preFields,
}: {
curFields: BaseOutputStructLineType[];
matchFn: (filed: BaseOutputStructLineType) => boolean;
setter: (field: BaseOutputStructLineType, val: boolean) => void;
getter: (field: BaseOutputStructLineType) => boolean | undefined;
preFields?: BaseOutputStructLineType[];
}) => {
if (curFields.length <= 1) {
return;
}
const preMatchedFieldsId =
preFields?.filter(matchFn).map(field => field._id) || [];
curFields.forEach(field => {
if (preMatchedFieldsId.includes(field._id) && getter(field)) {
setter(field, false);
}
});
const leftMatchedFields = curFields.filter(matchFn);
if (leftMatchedFields.length <= 1) {
return;
}
leftMatchedFields.forEach((field, idx) => {
const targetVal = idx === leftMatchedFields.length - 1;
if (getter(field) !== targetVal) {
setter(field, targetVal);
}
});
};

View File

@@ -0,0 +1,174 @@
/*
* 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 { isNumber } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { InputComponentType } from '@coze-arch/bot-api/connector_api';
import {
type BaseOutputStructLineType,
type FeishuBaseConfigFe,
type InputConfigFe,
} from '../types';
import { INPUT_CONFIG_TEXT_MAX_CHAR } from '../constants';
import {
getIsSelectType,
getIsStructOutput,
verifyOutputStructFieldAsGroupByKey,
verifyOutputStructFieldAsPrimaryKey,
} from './utils';
export const validateFullConfig = (config: FeishuBaseConfigFe): boolean => {
if (!validateOutputConfig(config)) {
return false;
}
const inputValid = validateInputConfig(config);
// console.log('inputValid', inputValid);
return inputValid;
};
const validateOutputConfig = (config: FeishuBaseConfigFe) => {
if (!getIsStructOutput(config.output_type)) {
return isNumber(config.output_type);
}
const structFields = config.output_sub_component.item_list || [];
const structPatternVerified = validateOutputStructPattern(structFields);
if (!structPatternVerified) {
return false;
}
return validateOutputStructGroupByKeyAndPrimaryKey(structFields);
};
const validateOutputStructPattern = (
structFields: BaseOutputStructLineType[],
) => {
if (structFields.length < 1) {
return false;
}
return structFields.every(
field => !!field.key && isNumber(field.output_type),
);
};
const validateOutputStructGroupByKeyAndPrimaryKey = (
structFields: BaseOutputStructLineType[],
): boolean => {
const groupByKeyVerifyRes = validateOutputStructGroupByKey(structFields);
if (!groupByKeyVerifyRes.ok) {
return false;
}
const primaryKeyVerifyRes = validateOutputStructPrimaryKey(structFields);
return primaryKeyVerifyRes.ok;
};
interface StructOutputGroupByOrPrimaryKeyVerifyRes {
ok: boolean;
error: string;
}
export const validateOutputStructGroupByKey = (
structFields: BaseOutputStructLineType[],
): StructOutputGroupByOrPrimaryKeyVerifyRes => {
const fields = structFields.filter(field => field.is_group_by_key);
if (fields.length > 1) {
return {
ok: false,
error: I18n.t('publish_base_configFields_requiredWarn'),
};
}
const field = fields.at(0);
if (!field) {
return {
ok: false,
error: I18n.t('publish_base_configFields_requiredWarn'),
};
}
if (!verifyOutputStructFieldAsGroupByKey(field)) {
return {
ok: false,
error: '',
};
}
return { ok: true, error: '' };
};
export const validateOutputStructPrimaryKey = (
structFields: BaseOutputStructLineType[],
): StructOutputGroupByOrPrimaryKeyVerifyRes => {
const fields = structFields.filter(field => field.is_primary);
if (fields.length > 1) {
return {
ok: false,
error: I18n.t('publish_base_configFields_requiredWarn'),
};
}
const field = fields.at(0);
if (!field) {
return {
ok: false,
error: I18n.t('publish_base_configFields_requiredWarn'),
};
}
if (verifyOutputStructFieldAsPrimaryKey(field)) {
return { ok: true, error: '' };
}
return {
ok: false,
error: '',
};
};
const validateInputConfig = (config: FeishuBaseConfigFe) => {
if (!config.input_config.length) {
return false;
}
if (!validateInputFieldsCommonPattern(config.input_config)) {
return false;
}
return config.input_config.every(validateSingleInputFieldControl);
};
const validateInputFieldsCommonPattern = (inputConfigs: InputConfigFe[]) => {
if (!inputConfigs.every(cfg => cfg.title && cfg.input_component)) {
return false;
}
return !inputConfigs.some(cfg => cfg.invalid);
};
export const validateSingleInputFieldControl = (
inputControlConfig: InputConfigFe,
): boolean => {
const { type } = inputControlConfig.input_component;
if (!type) {
return false;
}
if (type === InputComponentType.Text) {
const maxChar = inputControlConfig.input_component.max_char;
return (
maxChar !== undefined &&
Number.isInteger(maxChar) &&
maxChar > 0 &&
maxChar <= INPUT_CONFIG_TEXT_MAX_CHAR
);
}
if (getIsSelectType(type)) {
return (
inputControlConfig.input_component.choice?.length > 0 &&
inputControlConfig.input_component.choice.every(x => !!x.name.trim())
);
}
return !!inputControlConfig.input_component.supported_type?.length;
};

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InputComponentType } from '@coze-arch/bot-api/connector_api';
import { type BaseOutputStructLineType } from '../types';
const OUTPUT_TYPE_STRUCT = 25;
export const OUTPUT_TYPE_TEXT = 1;
const OUTPUT_TYPE_NUMBER = 2;
export const getIsStructOutput = (id: number): boolean =>
id === OUTPUT_TYPE_STRUCT;
export const getIsTextOutput = (id: number | undefined): boolean =>
id === OUTPUT_TYPE_TEXT;
export const getIsNumberOutput = (id: number | undefined): boolean =>
id === OUTPUT_TYPE_NUMBER;
export const getIsSelectType = (type: InputComponentType) =>
[InputComponentType.SingleSelect, InputComponentType.MultiSelect].includes(
type,
);
export const verifyOutputStructFieldAsGroupByKey = (
field: BaseOutputStructLineType,
) => getIsTextOutput(field.output_type);
export const verifyOutputStructFieldAsPrimaryKey = (
field: BaseOutputStructLineType,
) => {
const outputType = field.output_type;
return getIsTextOutput(outputType) || getIsNumberOutput(outputType);
};

View File

@@ -0,0 +1,134 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"types": [],
"strictNullChecks": true,
"noImplicitAny": true,
"noUncheckedIndexedAccess": true,
"strictBindCallApply": true,
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../arch/bot-api/tsconfig.build.json"
},
{
"path": "../../arch/bot-error/tsconfig.build.json"
},
{
"path": "../../arch/bot-flags/tsconfig.build.json"
},
{
"path": "../../arch/bot-hooks/tsconfig.build.json"
},
{
"path": "../../arch/bot-http/tsconfig.build.json"
},
{
"path": "../../arch/bot-md-box-adapter/tsconfig.build.json"
},
{
"path": "../../arch/bot-space-api/tsconfig.build.json"
},
{
"path": "../../arch/bot-store/tsconfig.build.json"
},
{
"path": "../../arch/bot-tea/tsconfig.build.json"
},
{
"path": "../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../arch/bot-utils/tsconfig.build.json"
},
{
"path": "../../arch/fetch-stream/tsconfig.build.json"
},
{
"path": "../../arch/i18n/tsconfig.build.json"
},
{
"path": "../../arch/logger/tsconfig.build.json"
},
{
"path": "../../arch/report-events/tsconfig.build.json"
},
{
"path": "../../arch/report-tti/tsconfig.build.json"
},
{
"path": "../bot-editor-context-store/tsconfig.build.json"
},
{
"path": "../bot-input-length-limit/tsconfig.build.json"
},
{
"path": "../chat-background/tsconfig.build.json"
},
{
"path": "../../common/assets/tsconfig.build.json"
},
{
"path": "../../common/chat-area/chat-area/tsconfig.build.json"
},
{
"path": "../../components/bot-icons/tsconfig.build.json"
},
{
"path": "../../components/bot-semi/tsconfig.build.json"
},
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/stylelint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../../data/common/e2e/tsconfig.build.json"
},
{
"path": "../../data/common/reporter/tsconfig.build.json"
},
{
"path": "../../data/common/utils/tsconfig.build.json"
},
{
"path": "../../data/knowledge/knowledge-modal-base/tsconfig.build.json"
},
{
"path": "../../data/knowledge/knowledge-resource-processor-core/tsconfig.build.json"
},
{
"path": "../../data/memory/database-creator/tsconfig.build.json"
},
{
"path": "../../foundation/global-store/tsconfig.build.json"
},
{
"path": "../../studio/components/tsconfig.build.json"
},
{
"path": "../../studio/stores/bot-detail/tsconfig.build.json"
},
{
"path": "../../studio/user-store/tsconfig.build.json"
},
{
"path": "../tool-config/tsconfig.build.json"
},
{
"path": "../tool/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,20 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "stories", "vitest.config.ts", "tailwind.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"types": ["vitest/globals"],
"strictNullChecks": true,
"noImplicitAny": true,
"noUncheckedIndexedAccess": true,
"strictBindCallApply": true
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
});