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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

View File

@@ -0,0 +1,176 @@
.generate-list-wrap {
display: flex;
flex: 1;
align-items: center;
box-sizing: border-box;
height: 32px;
border-radius: 8px;
.hidden-element {
position: absolute;
width: 0;
height: 0;
background-image: url('../assets/bot-generate-loading-sprite.png'), url('../assets/bot-generate-dis-sprite.png')
}
.split-line{
width: 1px;
height: 16px;
margin: 0 12px;
background-color: var(--coz-stroke-plus);
}
.avatar {
position: relative;
width: 32px;
height: 32px;
border-radius: 8px;
&.checked::after {
content: '';
position: absolute;
top: 4px;
right: 4px;
width: 16px;
height: 16px;
background: url('../assets/list-checked-bold.png') no-repeat center center/cover;
}
.loading-mask {
cursor: pointer;
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: transparent;
border-radius: 8px;
&.loading {
background-image: url('../assets/bot-generate-loading-sprite.png');
background-repeat: no-repeat;
background-position: -2px;
background-size: 2190px 73px;
animation: loading 1.5s steps(30) infinite;
}
&.finish {
background-image: url('../assets/bot-generate-dis-sprite.png');
background-repeat: no-repeat;
background-position: -2px;
background-size: 730px 73px;
animation: finish .5s steps(10) forwards;
}
.mask {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #FFF;
background-color: var(--coz-mg-mask);
border-radius: 8px;
}
}
.avatar-img {
filter: brightness(1.5) blur(6px);
mix-blend-mode: hard-light;
animation: fade-in .8s .2s forwards;
img {
width: 100%;
object-fit: cover;
}
}
}
.generate-btn {
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 68px;
height: 68px;
font-size: 12px;
color: #4D53E8;
background-color: var(--coz-mg-primary);
border: 1px solid var(--coz-stroke-primary);
border-radius: 8px;
&.disabled {
cursor: not-allowed;
color: #B4BAF6;
svg {
opacity: .4;
}
}
span {
line-height: 16px;
}
}
}
@keyframes loading {
from {
background-position: -2px -2px;
}
to {
background-position: -2192px -2px;
}
}
@keyframes finish {
from {
background-position: -2px -2px;
}
to {
background-position: -730px -2px;
}
}
@keyframes fade-in {
0% {
filter: brightness(1.5) blur(6px);
mix-blend-mode: hard-light;
}
30% {
mix-blend-mode: unset;
}
100% {
filter: brightness(1) blur(0);
mix-blend-mode: unset;
}
}

View File

@@ -0,0 +1,322 @@
/*
* 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, useMemo, useRef, useState } from 'react';
import { isFunction } from 'lodash-es';
import classNames from 'classnames';
import axios, { type CancelTokenSource } from 'axios';
import { useHover } from 'ahooks';
import {
REPORT_EVENTS as ReportEventNames,
createReportEvent,
} from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import {
IconCozCheckMark,
IconCozCrossCircle,
} from '@coze-arch/coze-design/icons';
import {
Tooltip,
Toast,
Image,
AIButton,
Space,
} from '@coze-arch/coze-design';
import { loadImage } from '@coze-arch/bot-utils';
import { DeveloperApi } from '@coze-arch/bot-api';
import s from './index.module.less';
type UploadValue = { uid: string; url: string }[];
interface GenerateInfo {
name: string;
desc?: string;
}
interface AutoGenerateProps {
onChange: (value?: UploadValue) => void;
generateInfo?: GenerateInfo | (() => GenerateInfo);
generateTooltip?: {
generateBtnText?: string;
contentNotLegalText?: string;
};
showAiAvatar: boolean;
/**
* 最多允许多少个候选
* @default 5
*/
maxCandidateCount?: number;
}
interface PictureItem {
url: string;
uid: string;
}
// 自动生成头像错误码
enum ErrorCode {
OVER_QUOTA_PER_DAY = 700012034,
CONTENT_NOT_LEGAL = 700012050,
}
const MAX_CANDIDATE_COUNT = 5;
const MAX_TOTAL_COUNT = 25;
// eslint-disable-next-line @coze-arch/max-line-per-function
export const AutoGenerate = (props: AutoGenerateProps) => {
const {
onChange,
generateInfo,
showAiAvatar,
generateTooltip,
maxCandidateCount = MAX_CANDIDATE_COUNT,
} = props;
const cancelTokenSource = useRef<CancelTokenSource>();
const hoverCount = useRef(0);
const loadingRef = useRef<HTMLDivElement>(null);
const loadingHover = useHover(loadingRef);
const [loading, setLoading] = useState(false);
const [totalCount, setTotalCount] = useState(0);
const [showGenerateBtn, setShowGenerateBtn] = useState(true);
const [pictureList, setPictureList] = useState<PictureItem[]>([]);
const [checkedId, setCheckedId] = useState(-1);
const tooltipContent = useMemo(() => {
if (totalCount >= MAX_TOTAL_COUNT && pictureList.length === 0) {
return I18n.t('bot_edit_profile_pircture_autogen_quota_tooltip');
}
const defaultText = IS_OVERSEA
? I18n.t('bot_edit_profile_pircture_autogen_tooltip')
: I18n.t('bot_edit_profile_pircture_autogen_tooltip_cn');
return generateTooltip?.generateBtnText || defaultText;
}, [totalCount, pictureList.length, generateTooltip?.generateBtnText]);
const allowGenerate = useMemo(
() =>
totalCount < MAX_TOTAL_COUNT &&
(isFunction(generateInfo) ? generateInfo?.().name : generateInfo?.name),
[generateInfo, totalCount],
);
const cancelGenerate = () => {
cancelTokenSource.current?.cancel('cancel generate picture');
};
const updateParentValue = (id: number, value: UploadValue) => {
setCheckedId(id);
onChange(value);
};
const getPicture = async () => {
hoverCount.current = 1;
setLoading(true);
const newList = [
...pictureList,
{
url: '',
uid: '',
},
];
setPictureList(newList);
const reportEvent = createReportEvent({
eventName: ReportEventNames.botGetAiGenerateAvatar,
});
try {
cancelTokenSource.current = axios.CancelToken.source();
const { name, desc } = isFunction(generateInfo)
? generateInfo()
: generateInfo || {};
const { data } = await DeveloperApi.GenerateIcon(
{
bot_name: name,
description: desc,
},
{
__disableErrorToast: true,
cancelToken: cancelTokenSource.current.token,
},
);
setTotalCount(Number(data?.count));
await loadImage(String(data?.icon_url));
setLoading(false);
setPictureList(prevList => {
prevList[prevList.length - 1] = {
url: String(data?.icon_url),
uid: String(data?.icon_uri),
};
return prevList;
});
updateParentValue(newList.length - 1, [
{
url: String(data?.icon_url),
uid: String(data?.icon_uri),
},
]);
reportEvent.success();
} catch (error) {
onChange();
setLoading(false);
setPictureList(list => {
list.pop();
return list;
});
const codeNumber = Number((error as { code: number })?.code);
if (codeNumber === ErrorCode.OVER_QUOTA_PER_DAY) {
// 超过单日次数上限
setTotalCount(MAX_TOTAL_COUNT);
Toast.error({
content: I18n.t('bot_edit_profile_pircture_autogen_quota_tooltip'),
showClose: false,
});
reportEvent.error({
reason: 'The number of times in a day exceeded the upper limit',
error: error instanceof Error ? error : void 0,
});
} else if (codeNumber === ErrorCode.CONTENT_NOT_LEGAL) {
Toast.error({
content:
generateTooltip?.contentNotLegalText ||
I18n.t('generate_bot_icon_content_filter'),
showClose: false,
});
reportEvent.error({
reason:
"The bot's name or description contains inappropriate content",
error: error instanceof Error ? error : void 0,
});
} else if (codeNumber > 0) {
Toast.error({
content: I18n.t('error'),
showClose: false,
});
reportEvent.error({
reason: 'Failed to generate profile picture',
error: error instanceof Error ? error : void 0,
});
}
}
};
useEffect(() => {
// 获取当日总生成次数
DeveloperApi.GetGenerateIconInfo()
.then(({ data }) => {
setTotalCount(Number(data?.current_day_count));
})
.catch(error => {
console.log(error);
});
}, []);
useEffect(() => {
if (
pictureList.length >= maxCandidateCount ||
(totalCount >= MAX_TOTAL_COUNT && pictureList.length > 0)
) {
setShowGenerateBtn(false);
} else {
setShowGenerateBtn(true);
}
}, [pictureList.length, totalCount]);
useEffect(() => {
if (loadingRef.current) {
hoverCount.current++;
}
}, [loadingHover]);
useEffect(() => {
if (!showAiAvatar) {
setCheckedId(-1);
if (loading) {
cancelGenerate();
}
}
}, [showAiAvatar]);
return (
<div className={s['generate-list-wrap']}>
<div className={s['hidden-element']} />
<div className={s['split-line']} />
<Space spacing={4}>
{(pictureList || []).map((picture, idx) => (
<div
key={idx}
className={classNames(s.avatar)}
onClick={() => {
if (picture.url) {
updateParentValue(idx, [
{ url: picture.url, uid: String(picture.uid) },
]);
}
}}
>
<div
ref={loadingRef}
className={classNames(s['loading-mask'], {
[s.loading]: !picture.url,
[s.finish]: picture.url,
[s['loading-hover']]: loadingHover && !picture.url,
})}
>
{/* 二次hover展示取消 */}
{hoverCount.current > 1 && loadingHover && !picture.url ? (
<div
className={s.mask}
onClick={e => {
e.stopPropagation();
cancelGenerate();
}}
>
<IconCozCrossCircle />
</div>
) : null}
{/* 选中图片蒙版 */}
{checkedId === idx && (
<div className={s.mask}>
<IconCozCheckMark className="text-[16]" />
</div>
)}
</div>
<Image
className={picture.url && s['avatar-img']}
preview={false}
src={picture.url}
/>
</div>
))}
{showGenerateBtn ? (
<Tooltip position="topLeft" content={tooltipContent}>
<AIButton
onlyIcon
color="aihglt"
disabled={!allowGenerate}
loading={loading}
onClick={() => {
if (allowGenerate) {
getPicture();
}
}}
/>
</Tooltip>
) : null}
</Space>
</div>
);
};

View File

@@ -0,0 +1,127 @@
.upload {
overflow: hidden;
width: fit-content;
height: 64px;
margin: auto;
.circle {
:global {
.semi-upload-picture-file-card-uploading::before {
border-radius: 50%;
}
img {
border-radius: 50%;
}
}
}
.avatar {
width: 64px;
height: 64px;
border-radius: 14px;
>img {
width: 100%;
height: 100%;
object-fit: cover;
}
:global {
.semi-skeleton-image {
background-color: #fff;
border: 1px solid rgb(29 28 35 / 8%);
border-radius: 14px;
}
}
}
}
.upload-button-wrap {
display: flex;
justify-content: center;
width: 100%;
}
.upload-button {
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
.avatar-wrap {
position: relative;
width: 64px;
height: 64px;
.mask {
cursor: pointer;
&.full-center {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: rgba(255, 255, 255, 0%);
visibility: hidden;
background-color: rgba(22, 22, 26, 0%);
border-radius: 14px;
transition: all 0.1s;
}
&.right-bottom {
position: absolute;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 4px 0 0 4px;
color: #fff;
}
}
&:hover {
.mask {
&.full-center {
color: #fff;
visibility: visible;
background-color: var(--coz-mg-mask);
}
}
}
}
.upload-with-auto-generate {
display: flex;
align-items: flex-end;
.upload {
height: 64px;
margin: 0;
.avatar {
width: 64px;
height: 64px;
}
}
.avatar-wrap {
width: 64px;
height: 64px;
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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 {
PictureUpload,
type GenerateInfo,
type UploadValue,
} from './picture-upload';
export { default as customUploadRequest } from './utils/custom-upload-request';
export { type RenderAutoGenerateParams } from './picture-upload';

View File

@@ -0,0 +1,296 @@
/*
* 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 complexity */
/* eslint-disable @typescript-eslint/no-magic-numbers */
/* eslint-disable react-hooks/rules-of-hooks */
import { useMemo, useRef, useState, type FC } from 'react';
import classNames from 'classnames';
import { useMount } from 'ahooks';
import { CommonE2e } from '@coze-data/e2e';
import { I18n } from '@coze-arch/i18n';
import { IconCozEdit } from '@coze-arch/coze-design/icons';
import { type FileItem, type UploadProps } from '@coze-arch/bot-semi/Upload';
import { type CommonFieldProps } from '@coze-arch/bot-semi/Form';
import { UIButton, Toast, withField, Image, Upload } from '@coze-arch/bot-semi';
import { IconAvatarEditMask } from '@coze-arch/bot-icons';
import { type FileBizType, IconType } from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import customUploadRequest from './utils/custom-upload-request';
import s from './index.module.less';
export type UploadValue = { uid: string | undefined; url: string }[];
export interface GenerateInfo {
name: string;
desc?: string;
}
export interface RenderAutoGenerateParams {
uploadPicture: () => void;
showAiAvatar: boolean;
setShowAiAvatar: (show: boolean) => void;
generateInfo?: GenerateInfo | (() => GenerateInfo);
generateTooltip?: {
generateBtnText?: string;
contentNotLegalText?: string;
};
onChange?: (value: UploadValue) => void;
maxCandidateCount?: number;
}
interface PackageUploadProps {
value?: FileItem[];
onChange?: (value: UploadValue) => void;
fileBizType: FileBizType;
uploadButtonText?: string;
iconType?: IconType;
disabled?: boolean;
avatarClassName?: string;
uploadClassName?: string;
triggerClassName?: string;
maskIcon?: React.ReactNode;
/**
* 编辑遮罩的展示模式
* - full-center(默认): 整体覆盖黑色透明遮罩, Icon 居中展示. hover 展示
* - right-bottom: 右下角遮罩, 长期展示
*/
maskMode?: 'full-center' | 'right-bottom';
/** 编辑遮罩的 className */
editMaskClassName?: string;
/** max size */
maxSize?: number;
withAutoGenerate?: boolean;
generateInfo?: GenerateInfo | (() => GenerateInfo);
generateTooltip?: {
generateBtnText?: string;
contentNotLegalText?: string;
};
/**
* 自动生成的最大候选数量
* @default 5
*/
maxCandidateCount?: number;
beforeUploadCustom?: () => void;
afterUploadCustom?: () => void;
accept?: string;
onGenerateStaticImageClick?: React.MouseEventHandler<HTMLButtonElement>;
onGenerateGifClick?: React.MouseEventHandler<HTMLButtonElement>;
onSizeError?: () => void;
// 自定义自定生成图片逻辑
renderAutoGenerate?: (params: RenderAutoGenerateParams) => React.ReactNode;
testId?: string;
}
// eslint-disable-next-line @coze-arch/max-line-per-function
const _PictureUpload = (props: PackageUploadProps) => {
// 业务
const {
onChange,
value,
fileBizType,
uploadButtonText,
iconType = IconType.Bot,
disabled = false,
avatarClassName,
uploadClassName,
triggerClassName,
maskIcon,
maskMode = 'full-center',
editMaskClassName,
withAutoGenerate = false,
generateInfo,
generateTooltip,
beforeUploadCustom,
afterUploadCustom,
accept = 'image/*',
maxCandidateCount,
renderAutoGenerate,
onSizeError,
maxSize = 2 * 1024,
testId,
} = props;
const uploadRef = useRef<Upload>(null);
const pictureValue = value?.at(0);
const [loadingIcon, setLoadingIcon] = useState(!pictureValue);
const [showAiAvatar, setShowAiAvatar] = useState(withAutoGenerate);
const maskIconInner = useMemo(() => {
if (maskIcon) {
return maskIcon;
}
return (
<IconCozEdit
className={classNames(
maskMode === 'right-bottom' ? 'text-[14px]' : 'text-[24px]',
)}
/>
);
}, [maskIcon, maskMode]);
const getIcon = async () => {
setLoadingIcon(true);
try {
const res = await DeveloperApi.GetIcon({
icon_type: iconType,
});
const iconData = res.data?.icon_list?.[0];
if (!iconData) {
Toast.error({
content: I18n.t('error'),
showClose: false,
});
return;
}
const { url = '', uri = '' } = iconData;
onChange?.([
{
url,
uid: uri,
},
]);
} catch (e) {
Toast.error({
content: I18n.t('error'),
showClose: false,
});
}
};
useMount(() => {
if (!pictureValue) {
getIcon().then(() => setLoadingIcon(false));
}
});
const customRequest: UploadProps['customRequest'] = options => {
customUploadRequest({
...options,
fileBizType,
onSuccess: data => {
if (withAutoGenerate) {
setShowAiAvatar(false);
}
options.onSuccess(data);
onChange?.([
{
uid: data?.upload_uri || '',
url: data?.upload_url || '',
},
]);
},
beforeUploadCustom,
afterUploadCustom,
});
};
const uploadPicture = () => {
uploadRef.current?.openFileDialog();
};
return (
<div
className={withAutoGenerate ? s['upload-with-auto-generate'] : ''}
data-testid={CommonE2e.PictureUpload}
>
<Upload
action=""
className={classNames(s.upload, uploadClassName)}
limit={1}
customRequest={customRequest}
fileList={value}
accept={accept}
showReplace={false}
showUploadList={false}
ref={uploadRef}
disabled={disabled}
maxSize={maxSize}
onSizeError={() => {
if (onSizeError) {
onSizeError();
return;
}
Toast.error({
// starling 切换
content: I18n.t(
'dataset_upload_image_warning',
{},
'Please upload an image less than 2MB',
),
showClose: false,
});
}}
>
<div
className={classNames(
s['avatar-wrap'],
'cursor-pointer',
triggerClassName,
)}
data-testid={testId}
>
<Image
preview={false}
className={classNames(
s.avatar,
loadingIcon && s['avatar-loading'],
avatarClassName,
)}
placeholder={
<Image
className={classNames(s.avatar, avatarClassName)}
src={pictureValue?.url}
preview={false}
/>
}
/>
<div className={classNames(s.mask, s[maskMode], editMaskClassName)}>
{maskMode === 'right-bottom' && (
<IconAvatarEditMask className="absolute inset-0 w-full h-full rounded-br-[14px] overflow-hidden" />
)}
<div className="relative inline-flex">{maskIconInner}</div>
</div>
</div>
</Upload>
{uploadButtonText && !disabled ? (
<div className={s['upload-button-wrap']}>
<UIButton
className={s['upload-button']}
theme="borderless"
type="primary"
onClick={uploadPicture}
>
{uploadButtonText}
</UIButton>
</div>
) : null}
{withAutoGenerate && renderAutoGenerate
? renderAutoGenerate({
uploadPicture,
showAiAvatar,
setShowAiAvatar,
generateInfo,
generateTooltip,
onChange,
maxCandidateCount,
})
: null}
</div>
);
};
export const PictureUpload: FC<CommonFieldProps & PackageUploadProps> =
withField(_PictureUpload);

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { I18n } from '@coze-arch/i18n';
import { type customRequestArgs } from '@coze-arch/bot-semi/Upload';
import { CustomError } from '@coze-arch/bot-error';
import {
type UploadFileData,
type FileBizType,
} from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import getBase64 from './get-base64';
function customUploadRequest(
options: Omit<customRequestArgs, 'onSuccess'> & {
fileBizType: FileBizType;
onSuccess: (data?: UploadFileData) => void;
beforeUploadCustom?: () => void;
afterUploadCustom?: () => void;
},
): void {
const {
onSuccess,
onError,
file,
beforeUploadCustom,
afterUploadCustom,
fileBizType,
} = options;
if (typeof file === 'string') {
return;
}
beforeUploadCustom?.();
const getFileExtension = (name: string) => {
const index = name.lastIndexOf('.');
return name.slice(index + 1);
};
try {
const { fileInstance } = file;
// 业务
if (fileInstance) {
const extension = getFileExtension(file.name);
// 业务
(async () => {
try {
const base64 = await getBase64(fileInstance);
const result = await DeveloperApi.UploadFile({
file_head: {
file_type: extension,
biz_type: fileBizType,
},
data: base64,
});
onSuccess?.(result.data);
afterUploadCustom?.();
} catch (error) {
// 如参数校验失败情况会走到catch
afterUploadCustom?.();
}
})();
} else {
afterUploadCustom?.();
throw new CustomError(ReportEventNames.parmasValidation, I18n.t('error'));
}
} catch (e) {
afterUploadCustom?.();
onError?.({
status: 0,
});
}
}
export default customUploadRequest;

View File

@@ -0,0 +1,37 @@
/*
* 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 { REPORT_EVENTS as ReportEventNames } from '@coze-arch/report-events';
import { CustomError } from '@coze-arch/bot-error';
function getBase64(file: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = event => {
const result = event.target?.result;
if (!result || typeof result !== 'string') {
reject(
new CustomError(ReportEventNames.parmasValidation, 'file read fail'),
);
return;
}
resolve(result.replace(/^.*?,/, ''));
};
fileReader.readAsDataURL(file);
});
}
export default getBase64;