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,237 @@
/*
* 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 MouseEvent, useEffect, useRef, useState } from 'react';
import { get } from 'lodash-es';
import { useBotInfoStore } from '@coze-studio/bot-detail-store/bot-info';
import { I18n } from '@coze-arch/i18n';
import { IconCozTamplate } from '@coze-arch/coze-design/icons';
import {
Button,
Form,
type FormApi,
IconButton,
Popover,
Typography,
} from '@coze-arch/coze-design';
import { type GenerateUserQueryCollectPolicyRequest } from '@coze-arch/bot-api/playground_api';
import { Tips } from './tips';
import s from './index.module.less';
const options = [
{
label: I18n.t('bot_dev_privacy_setting_conversation'),
value: I18n.t('bot_dev_privacy_setting_conversation'),
},
];
const defaultOptionsValue = [I18n.t('bot_dev_privacy_setting_conversation')];
interface GenerateByTemplateProps {
handleGenerate: (v: GenerateUserQueryCollectPolicyRequest) => void;
loading: boolean;
templateLink: string;
link: string;
}
// eslint-disable-next-line @coze-arch/max-line-per-function
export const GenerateByTemplate: FC<GenerateByTemplateProps> = ({
handleGenerate,
loading,
templateLink,
link,
}) => {
const { botId } = useBotInfoStore($store => ({
botId: $store.botId,
}));
const [visible, setVisible] = useState(false);
const [configInfo, setConfigInfo] =
useState<GenerateUserQueryCollectPolicyRequest>();
const [isFailToValid, setIsFailToValid] = useState(true);
const formApi = useRef<FormApi<GenerateUserQueryCollectPolicyRequest>>();
const onFormValueChange = (values: GenerateUserQueryCollectPolicyRequest) => {
const developerName = get(values, 'developer_name');
const contactInformation = get(values, 'contact_information');
setIsFailToValid(!(developerName && contactInformation));
setConfigInfo({
...values,
});
};
const onVisibleChange = (isVisble: boolean) => {
if (isVisble) {
setDefaultValue();
}
};
const setDefaultValue = () => {
if (configInfo) {
formApi.current?.setValue('developer_name', configInfo.developer_name);
formApi.current?.setValue(
'contact_information',
configInfo.contact_information,
);
}
};
useEffect(() => {
if (link) {
setConfigInfo({ developer_name: '', contact_information: '' });
setVisible(false);
}
}, [link]);
const onClickGenerate = () => {
handleGenerate({ ...configInfo, bot_id: botId });
};
const onOpen = (e: MouseEvent) => {
e.stopPropagation();
setVisible(true);
};
return (
<Popover
position="right"
trigger="custom"
stopPropagation={true}
onVisibleChange={onVisibleChange}
visible={visible}
onClickOutSide={() => setVisible(false)}
content={
<div className="p-[16px] w-[320px]">
<div className="coz-fg-plus text-[20px] font-medium leading-[32px]">
{I18n.t('bot_dev_privacy_setting_privacy_template_1')}
</div>
<div className="coz-fg-primary text-[14px] font-normal leading-[20px] pb-[12px]">
{I18n.t('bot_dev_privacy_setting_privacy_template_2', {
privacy_template: (
<Typography.Text link onClick={() => window.open(templateLink)}>
{I18n.t('bot_dev_privacy_setting_privacy_template_3')}
</Typography.Text>
),
})}
</div>
<div>
<Form<GenerateUserQueryCollectPolicyRequest>
getFormApi={api => (formApi.current = api)}
labelPosition="top"
showValidateIcon={false}
className={s['form-wrap']}
onValueChange={values =>
onFormValueChange(
values as GenerateUserQueryCollectPolicyRequest,
)
}
autoComplete="off"
disabled={loading}
>
<Form.Input
field="developer_name"
label={I18n.t('bot_dev_privacy_setting_developer_name')}
style={{ width: '100%' }}
trigger="blur"
maxLength={50}
placeholder={I18n.t(
'bot_dev_privacy_setting_developer_collect3',
)}
rules={[
{
required: true,
message: I18n.t(
'bot_dev_privacy_setting_developer_collect3',
),
},
]}
/>
<Form.Select
field="collect_detail"
label={{
text: I18n.t('bot_dev_privacy_setting_developer_collect1'),
extra: (
<Tips
content={I18n.t(
'bot_dev_privacy_setting_developer_collect7',
)}
size="small"
/>
),
}}
optionList={options}
disabled
initValue={defaultOptionsValue}
style={{ width: '100%' }}
placeholder={I18n.t(
'bot_dev_privacy_setting_developer_collect4',
)}
rules={[
{
required: true,
message: I18n.t(
'bot_dev_privacy_setting_developer_collect4',
),
},
]}
/>
<Form.Input
field="contact_information"
label={I18n.t('bot_dev_privacy_setting_developer_collect2')}
style={{ width: '100%' }}
trigger="blur"
maxLength={50}
placeholder={I18n.t(
'bot_dev_privacy_setting_developer_collect5',
)}
rules={[
{
required: true,
message: I18n.t(
'bot_dev_privacy_setting_developer_collect5',
),
},
]}
/>
</Form>
</div>
<div className="flex justify-end mt-[12px]">
<Button
loading={loading}
onClick={onClickGenerate}
disabled={isFailToValid}
>
{I18n.t(
loading
? 'bot_dev_privacy_setting_generate_link2'
: 'bot_dev_privacy_setting_generate_link1',
)}
</Button>
</div>
</div>
}
>
<IconButton
icon={<IconCozTamplate />}
iconPosition="left"
color="secondary"
size="small"
onClick={onOpen}
>
{I18n.t('bot_dev_privacy_setting_privacy_template')}
</IconButton>
</Popover>
);
};

View File

@@ -0,0 +1,96 @@
/* stylelint-disable max-nesting-depth */
/* stylelint-disable selector-class-pattern */
.query-collect-modal {
:global {
.semi-modal-header {
align-items: center;
.semi-button-borderless {
width: 40px;
height: 40px;
padding: 11px;
&:hover {
background-color: rgba(var(--coze-bg-6), var(--coze-bg-6-alpha));
}
&:active {
background-color: rgba(var(--coze-bg-8), var(--coze-bg-8-alpha));
}
.semi-button-content {
color: rgba(var(--coze-fg-2), var(--coze-fg-2-alpha));
.semi-icon {
font-size: 18px;
}
}
}
}
.semi-modal-footer {
margin: 0;
}
.semi-switch:not(.semi-switch-checked){
background-color: var(--semi-color-fill-0);
}
.semi-switch:not(.semi-switch-checked):hover {
background-color: var(--semi-color-fill-1);
}
}
}
.form-wrap {
:global {
.semi-form-field {
padding: 0;
padding-bottom: 16px;
}
.semi-form-field-label {
margin-bottom: 6px;
padding: 0 8px;
font-size: 12px;
font-weight: 500;
font-style: normal;
line-height: 16px;
color: var(--coz-fg-secondary);
}
.semi-form-field-error-message {
padding-left: 8px;
}
.semi-input-prefix-text {
font-size: 12px;
font-weight: 400;
color: var(--coz-fg-secondary);
}
.semi-input-wrapper {
background-color: transparent;
}
.semi-input-suffix {
.coz-icon-button {
display: flex;
padding-right: 4px;
.coz-button.coz-btn-small{
border-radius: 6px;
}
}
}
.semi-form-field-label-required .semi-form-field-label-text::after{
margin-left: 0;
}
.semi-input-wrapper__with-suffix .semi-input{
padding-right: 4px;
}
}
}

View File

@@ -0,0 +1,201 @@
/*
* 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, useEffect, useRef, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { CollapsibleIconButton } from '@coze-studio/components/collapsible-icon-button';
import { updateQueryCollect } from '@coze-studio/bot-detail-store/save-manager';
import { useQueryCollectStore } from '@coze-studio/bot-detail-store/query-collect';
import { useBotDetailIsReadonly } from '@coze-studio/bot-detail-store';
import { I18n } from '@coze-arch/i18n';
import {
useGenerateLink,
useGetUserQueryCollectOption,
} from '@coze-agent-ide/space-bot/hook';
import { IconCozEye } from '@coze-arch/coze-design/icons';
import { Modal, Switch, Form, type FormApi } from '@coze-arch/coze-design';
import { getUrlValue, isValidUrl } from './utils';
import { Tips } from './tips';
import { GenerateByTemplate } from './generate-by-template';
import s from './index.module.less';
const itemKey = Symbol.for('QueryCollect');
// eslint-disable-next-line @coze-arch/max-line-per-function
export const QueryCollect: FC = () => {
const { privatePolicy, isCollect, setQueryCollect } = useQueryCollectStore(
useShallow($store => ({
isCollect: $store.is_collected,
privatePolicy: $store.private_policy,
setQueryCollect: $store.setQueryCollect,
})),
);
const { queryCollectOption, supportText } = useGetUserQueryCollectOption();
const isReadonly = useBotDetailIsReadonly();
const { link, loading, runGenerate } = useGenerateLink();
const formApi = useRef<FormApi<{ policyLink: string }>>();
const [visible, setVisible] = useState(false);
const [checked, setChecked] = useState(false);
const [privacyUrl, setPrivacyUrl] = useState(privatePolicy);
const privacyErrMsg = useRef('');
const onClose = () => {
setVisible(false);
};
const onOk = async () => {
const policyLink = formApi.current?.getValue('policyLink');
const queryCollectConf = {
is_collected: checked,
// cp-disable-next-line
private_policy: checked ? `https://${policyLink}` : '',
};
const {
data: { check_not_pass_msg, check_not_pass },
} = await updateQueryCollect(queryCollectConf);
privacyErrMsg.current = check_not_pass ? (check_not_pass_msg ?? '') : '';
await formApi.current?.validate();
setQueryCollect(queryCollectConf);
setVisible(false);
};
const openModal = () => {
setVisible(true);
};
useEffect(() => {
if (link) {
formApi.current?.setValue('policyLink', getUrlValue(link));
formApi.current?.validate();
}
}, [link]);
useEffect(() => {
setChecked(isCollect);
formApi.current?.setValue('policyLink', getUrlValue(privatePolicy ?? ''));
}, [visible, privatePolicy, isCollect]);
useEffect(() => {
privacyErrMsg.current = '';
}, [privacyUrl]);
return (
<>
<CollapsibleIconButton
itemKey={itemKey}
text={I18n.t('bot_dev_privacy_title')}
icon={<IconCozEye className="text-[16px]" />}
onClick={openModal}
/>
<Modal
width={480}
visible={visible}
onCancel={onClose}
maskClosable={false}
title={
<span className="text-[20px]">{I18n.t('bot_dev_privacy_title')}</span>
}
cancelText={I18n.t('cancel')}
okText={I18n.t('confirm')}
className={s['query-collect-modal']}
onOk={onOk}
okButtonProps={{
disabled: loading || isReadonly,
style: { marginLeft: '8px' },
}}
>
<div className="py-[16px]">
<div className="flex items-center justify-between py-[16px] pl-[12px] pr-[24px] rounded-[8px] border border-solid border-[var(--coz-stroke-plus)]">
<div className="flex items-center justify-center gap-[3px] ">
<span className="coz-fg-plus text-[14px] font-normal leading-[20px] ">
{I18n.t('bot_dev_privacy_setting_title')}
</span>
<Tips
content={
I18n.t('bot_dev_privacy_setting_channel') + supportText
}
size="medium"
/>
</div>
<Switch
checked={checked}
size="small"
onChange={v => setChecked(v)}
disabled={loading || isReadonly}
/>
</div>
<div className="coz-fg-secondary text-[12px] font-normal leading-[16px] px-[8px] pt-[2px] pb-[18px]">
{I18n.t('bot_dev_privacy_setting_desc')}
</div>
<div style={{ display: checked ? 'block' : 'none' }}>
<Form<{ policyLink: string }>
getFormApi={api => (formApi.current = api)}
labelPosition="top"
showValidateIcon={false}
className={s['form-wrap']}
autoComplete="off"
disabled={loading || isReadonly}
>
<Form.Input
field="policyLink"
label={I18n.t('bot_dev_privacy_setting_link1')}
style={{ width: '100%' }}
trigger="blur"
// cp-disable-next-line
prefix="https://"
stopValidateWithError
maxLength={50}
disabled={loading || isReadonly}
placeholder={I18n.t('privacy_link_placeholder')}
onChange={setPrivacyUrl}
suffix={
IS_OVERSEA || isReadonly ? null : (
<GenerateByTemplate
handleGenerate={runGenerate}
loading={loading}
templateLink={
queryCollectOption?.private_policy_template || ''
}
link={link}
/>
)
}
rules={[
{
validator: (_, value) => !checked || isValidUrl(value),
message: I18n.t('bot_dev_privacy_setting_invalid_link'),
},
{
validator: () => !checked || !privacyErrMsg.current,
message: () => privacyErrMsg.current,
},
]}
helpText={
<div className="coz-fg-secondary text-[12px] font-normal leading-[16px] px-[8px] pt-[2px]">
{I18n.t('bot_dev_privacy_setting_link2')}
</div>
}
/>
</Form>
</div>
</div>
</Modal>
</>
);
};

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from 'classnames';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import { Tooltip } from '@coze-arch/coze-design';
export const Tips = ({
content,
size = 'medium',
}: {
content: string;
size: 'small' | 'medium';
}) => (
<Tooltip content={content}>
<div
className={classNames(
size === 'small'
? 'w-[16px] h-[16px] rounded-[4px]'
: 'w-[24px] h-[24px] rounded-[8px]',
'flex items-center justify-center hover:coz-mg-secondary-hovered cursor-pointer',
)}
>
<IconCozInfoCircle className="coz-fg-secondary" />
</div>
</Tooltip>
);

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const DOMAIN_REGEXP = /^([0-9a-zA-Z-]{1,}\.)+([a-zA-Z]{2,})$/;
export function isValidUrl(url: string): boolean {
try {
// cp-disable-next-line
const urlObject = new URL(`https://${url}`);
return DOMAIN_REGEXP.test(urlObject.hostname);
// eslint-disable-next-line @coze-arch/use-error-in-catch -- 根据函数功能无需 throw error
} catch {
return false;
}
}
// cp-disable-next-line
export const getUrlValue = (url: string) => url?.replace(/^https:\/\//, '');