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,51 @@
/* stylelint-disable block-no-empty */
.upload-form {
.textarea-single-line {
:global {
.semi-input-textarea-counter {
position: absolute;
top: 4px;
right: 0;
}
}
}
.textarea-multi-line {
margin-bottom: 16px;
:global {
.semi-input-textarea-counter {}
}
}
.conversation-field {
padding: 0;
}
.upload-field {
padding-top: 0;
}
.schema_type {
padding-top: 0;
}
}
.upload-form-item {
:global {
.semi-form-field-label-text {
display: none;
}
}
}
.add-card {
background-color: white;
border-radius: 8px;
}
.add-card-inner {
display: flex;
flex-direction: column;
justify-content: center;
}

View File

@@ -0,0 +1,483 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/max-line-per-function */
/* eslint-disable max-lines-per-function -- refactor later */
import { useMemo, useRef, useState } from 'react';
import { isFunction } from 'lodash-es';
import { type WorkflowJSON } from '@flowgram-adapter/free-layout-editor';
import { type FrontWorkflowInfo } from '@coze-workflow/base/types';
import {
type BindBizType,
type SchemaType,
workflowApi,
WorkflowMode,
} from '@coze-workflow/base/api';
import {
WORKFLOW_NAME_MAX_LEN,
WORKFLOW_NAME_REGEX,
} from '@coze-workflow/base';
import { PictureUpload } from '@coze-common/biz-components/picture-upload';
import { type UploadValue } from '@coze-common/biz-components';
import { REPORT_EVENTS } from '@coze-arch/report-events';
import { reporter } from '@coze-arch/logger';
import { I18n } from '@coze-arch/i18n';
import { useUserInfo } from '@coze-arch/foundation-sdk';
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
import {
Button,
LoadingButton,
Space,
Toast,
Tooltip,
} from '@coze-arch/coze-design';
import { useSpaceStore } from '@coze-arch/bot-studio-store';
import { Form, Typography, UIFormTextArea, UIModal } from '@coze-arch/bot-semi';
import { CustomError } from '@coze-arch/bot-error';
import { FileBizType, IconType } from '@coze-arch/bot-api/developer_api';
import s from './index.module.less';
/** 输入合规校验异常的错误码 */
const sensitiveWordsErrorCode = ['702095075', '702095081'];
const { Checkbox } = Form;
export interface RuleItem {
validator: (rules: unknown[], value: string) => boolean | Error;
}
interface EditWorkFlowPropsInner {
/** 流程类型 */
flowMode?: WorkflowMode;
mode: 'update' | 'add';
visible: boolean;
// 默认confirm的disabled
/** 自定义弹窗标题 */
customTitleRender?: (title: React.ReactNode) => React.ReactNode;
initConfirmDisabled?: boolean;
workFlow?: FrontWorkflowInfo;
onSuccess?: (val: {
workflowId?: string;
flowMode?: EditWorkFlowPropsInner['flowMode'];
}) => void;
onCancel?: () => void;
/** @deprecated 未使用 */
spaceID?: string;
getLatestWorkflowJson?: () => Promise<WorkflowJSON>;
bindBizId?: string;
bindBizType?: BindBizType;
/** 当前项目 id只在项目内的 workflow 有该字段 */
projectId?: string;
nameValidators?: RuleItem[];
}
/** 表单值 */
interface FormValue {
icon_uri: UploadValue;
name: string;
target: string;
schema_type: SchemaType;
create_conversation?: boolean;
}
/** 获取弹窗标题 */
function getModalTitle(
mode: EditWorkFlowPropsInner['mode'],
flowMode: EditWorkFlowPropsInner['flowMode'],
): string {
switch (flowMode) {
case WorkflowMode.Imageflow:
return mode === 'add'
? I18n.t('imageflow_create')
: I18n.t('imageflow_edit');
case WorkflowMode.Workflow:
return mode === 'add'
? I18n.t('workflow_list_create_modal_title')
: I18n.t('workflow_list_edit_modal_title');
case WorkflowMode.ChatFlow:
return mode === 'add'
? I18n.t('wf_chatflow_81')
: I18n.t('wf_chatflow_84');
default:
return mode === 'add'
? I18n.t('workflow_list_create_modal_title')
: I18n.t('workflow_list_edit_modal_title');
}
}
const getPictureUploadInitValue = (
workFlow?: FrontWorkflowInfo,
): UploadValue | undefined => {
if (!workFlow) {
return;
}
return [
{
url: workFlow.url || '',
uid: workFlow.icon_uri || '',
},
];
};
export function CreateWorkflowModal({
flowMode = WorkflowMode.Workflow,
mode,
bindBizId,
bindBizType,
projectId,
visible,
workFlow,
initConfirmDisabled = false,
customTitleRender,
onSuccess,
onCancel,
nameValidators = [],
}: EditWorkFlowPropsInner) {
const formRef = useRef<Form<Partial<FormValue>>>(null);
const [confirmDisabled, setConfirmDisabled] = useState(initConfirmDisabled);
const [sensitiveTip, setSensitiveTip] = useState<string | undefined>();
const userInfo = useUserInfo();
const currentLocale = userInfo?.locale ?? navigator.language ?? 'en-US';
const getValues = async () => {
const formApi = formRef.current?.formApi;
await formApi?.validate(['name']);
return formApi?.getValues() as Partial<FormValue> | undefined;
};
const handleCancel = () => {
reporter.info({
message: 'workflow_info_modal_cancel',
namespace: 'workflow',
});
setSensitiveTip(undefined);
onCancel?.();
};
const handleError = (error: Error & { code?: string; msg?: string }) => {
if (sensitiveWordsErrorCode.includes(error.code || '')) {
setConfirmDisabled(true);
setSensitiveTip(error.msg);
return;
}
handleCancel();
throw error;
};
const handleUpdateWorkflow = async () => {
const workflowId = workFlow?.workflow_id;
reporter.info({
message: 'workflow_info_modal_confirm_update',
namespace: 'workflow',
meta: {
workflowId,
},
});
if (!workflowId) {
const msg = I18n.t('workflow_list_create_modal_workflow_id_empty');
throw new CustomError(REPORT_EVENTS.parmasValidation, msg);
}
const values = await getValues();
const updateParams = {
workflow_id: workflowId,
icon_uri: values?.icon_uri?.[0].uid || '',
name: values?.name,
desc: values?.target ? values.target : '',
space_id: workFlow.space_id || '',
// 更新头像等信息不需要重新test run
ignore_status_transfer: true,
schema_type: values?.schema_type || workFlow?.schema_type,
};
try {
await workflowApi.UpdateWorkflowMeta(updateParams);
reporter.info({
message: 'workflow_info_modal_update_success',
namespace: 'workflow',
});
Toast.success({
content: I18n.t('workflow_list_update_success'),
showClose: false,
});
await onSuccess?.({
workflowId,
flowMode,
});
} catch (error) {
reporter.error({
message: 'workflow_info_modal_update_fail',
namespace: 'workflow',
error,
});
handleError(error);
}
};
const handleCreateWorkflow = async () => {
reporter.info({
message: 'workflow_info_modal_confirm_create',
namespace: 'workflow',
});
const values = await getValues();
try {
const reqParams = {
...values,
space_id: useSpaceStore.getState().getSpaceId(),
name: values?.name || '',
desc: values?.target || '',
icon_uri: values?.icon_uri?.[0]?.uid || '',
flow_mode: flowMode,
bind_biz_id: bindBizId,
bind_biz_type: bindBizType,
project_id: projectId,
create_conversation: projectId
? values?.create_conversation
: undefined,
};
const resp = await workflowApi.CreateWorkflow(reqParams, {
headers: {
'x-locale': currentLocale,
},
});
const content =
flowMode === WorkflowMode.Imageflow
? I18n.t('imageflow_create_toast_success')
: flowMode === WorkflowMode.ChatFlow
? I18n.t('wf_chatflow_95')
: I18n.t('workflow_list_create_success');
Toast.success({
content,
showClose: false,
});
await onSuccess?.({
workflowId: resp.data?.workflow_id,
flowMode,
});
reporter.info({
message: 'workflow_info_modal_create_success',
namespace: 'workflow',
});
} catch (error) {
reporter.error({
message: 'workflow_info_modal_create_fail',
namespace: 'workflow',
error,
});
handleError(error);
}
};
const title = useMemo(() => {
const modelTitle = getModalTitle(mode, flowMode);
if (customTitleRender && isFunction(customTitleRender)) {
return customTitleRender(modelTitle);
}
return modelTitle;
}, [mode, flowMode, customTitleRender]);
const labels = useMemo<{
nameLabel?: string;
namePlaceholder?: string;
descLabel?: string;
descPlaceholder?: string;
nameFormatRuleLabel?: string;
nameRequiredLabel?: string;
descRequiredLabel?: string;
}>(() => {
if (flowMode === WorkflowMode.Imageflow) {
return {
nameLabel: I18n.t('imageflow_create_name'),
namePlaceholder: I18n.t('imageflow_create_name_placeholder'),
descLabel: I18n.t('imageflow_create_description'),
descPlaceholder: I18n.t('imageflow_create_description_placeholder'),
nameRequiredLabel: I18n.t('imageflow_create_name_placeholder'),
nameFormatRuleLabel: I18n.t('imageflow_create_name_wrong_format'),
descRequiredLabel: I18n.t('imageflow_create_description_placeholder'),
};
}
if (flowMode === WorkflowMode.ChatFlow) {
return {
nameLabel: I18n.t('wf_chatflow_85'),
namePlaceholder: I18n.t('wf_chatflow_91'),
descLabel: I18n.t('wf_chatflow_86'),
descPlaceholder: I18n.t('wf_chatflow_92'),
nameRequiredLabel: I18n.t('wf_chatflow_93'),
nameFormatRuleLabel: I18n.t('wf_chatflow_94'),
descRequiredLabel: I18n.t('wf_chatflow_122'),
};
}
return {
nameLabel: I18n.t('workflow_list_create_modal_name_label'),
namePlaceholder: I18n.t('workflow_list_create_modal_name_placeholder'),
descLabel: I18n.t('workflow_list_create_modal_description_label'),
descPlaceholder: I18n.t(
'workflow_list_create_modal_description_placeholder',
),
nameRequiredLabel: I18n.t(
'workflow_list_create_modal_name_rule_required',
),
nameFormatRuleLabel: I18n.t('workflow_list_create_modal_name_rule_reg'),
descRequiredLabel: I18n.t(
'workflow_list_create_modal_description_rule_required',
),
};
}, [flowMode]);
const iconType = useMemo(() => {
switch (flowMode) {
case WorkflowMode.Imageflow:
return IconType.Imageflow;
case WorkflowMode.Workflow:
return IconType.Workflow;
case WorkflowMode.ChatFlow:
return IconType.ChatFlow;
default:
return IconType.Workflow;
}
}, [flowMode]);
return (
<UIModal
type="action-small"
keepDOM={false}
icon={null}
visible={visible}
onCancel={handleCancel}
title={title}
footer={
<Space>
<Button
className="min-w-[96px]"
color="primary"
onClick={handleCancel}
data-testid="workflow.list.create.cancel"
>
{I18n.t('workflow_list_create_modal_footer_cancel')}
</Button>
<LoadingButton
className="min-w-[96px]"
color="hgltplus"
disabled={confirmDisabled}
onClick={
mode === 'add' ? handleCreateWorkflow : handleUpdateWorkflow
}
data-testid="workflow.list.create.submit"
>
{I18n.t('workflow_list_create_modal_footer_confirm')}
</LoadingButton>
</Space>
}
>
<Form<Partial<FormValue>>
ref={formRef}
showValidateIcon={false}
className={s['upload-form']}
onValueChange={({ name, target }) => {
setSensitiveTip(undefined);
setConfirmDisabled(!name?.trim() || !target?.trim());
}}
>
<PictureUpload
noLabel
fieldClassName={s['upload-field']}
field="icon_uri"
initValue={getPictureUploadInitValue(workFlow)}
iconType={iconType}
fileBizType={FileBizType.BIZ_BOT_WORKFLOW}
/>
<UIFormTextArea
stopValidateWithError
className={s['textarea-single-line']}
field="name"
placeholder={labels.namePlaceholder}
label={labels.nameLabel}
// noErrorMessage
initValue={workFlow?.name}
rows={1}
maxCount={WORKFLOW_NAME_MAX_LEN}
maxLength={WORKFLOW_NAME_MAX_LEN}
rules={[
{
required: true,
message: labels.nameRequiredLabel,
},
{
validator(_, value) {
if (!WORKFLOW_NAME_REGEX.test(value)) {
return new CustomError(
REPORT_EVENTS.formValidation,
labels.nameFormatRuleLabel ?? '',
);
}
return true;
},
},
...nameValidators,
]}
data-testid="workflow.list.create.name.input"
/>
{/* 只有项目内创建 Chatflow 时才可以绑定会话 */}
{mode === 'add' && projectId && flowMode === WorkflowMode.ChatFlow ? (
<Checkbox
fieldClassName={s['conversation-field']}
noLabel
initValue={true}
field="create_conversation"
>
<Typography.Text className="coz-fg-primary">
{I18n.t('wf_chatflow_87')}
</Typography.Text>
<Tooltip
position="top"
theme="dark"
style={{ width: 278 }}
content={I18n.t('wf_chatflow_82')}
>
<IconCozInfoCircle className="text-[16px] ml-1.5 coz-fg-dim" />
</Tooltip>
</Checkbox>
) : null}
<UIFormTextArea
field="target"
className={s['textarea-multi-line']}
label={labels.descLabel}
placeholder={labels.descPlaceholder}
initValue={workFlow?.desc}
maxCount={600}
maxLength={600}
rules={[
{
required: true,
message: labels.descRequiredLabel,
},
]}
data-testid="workflow.list.create.desc.input"
/>
{typeof sensitiveTip === 'string' ? (
<Form.ErrorMessage error={sensitiveTip} />
) : null}
</Form>
</UIModal>
);
}