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,30 @@
/*
* 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 { typeSafeJSONParse } from './safe-json-parse';
export function arrayBufferToObject(
buffer: ArrayBuffer,
encoding = 'utf-8',
): Record<string, unknown> {
try {
const decoder = new TextDecoder(encoding);
const string = decoder.decode(buffer);
return typeSafeJSONParse(string) as Record<string, unknown>;
} catch (error) {
return {};
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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 { isFunction } from 'lodash-es';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Obj = Record<string, any>;
// eslint-disable-next-line @typescript-eslint/naming-convention
export const ArrayUtil = {
array2Map,
mapAndFilter,
};
// region array2Map 重载声明
// 和 OptionUtil.array2Map 虽然相似,但在用法和类型约束上还是很不一样的
/**
* 将列表转化为 map
* @param items
* @param key 指定 item[key] 作为 map 的键
* @example
* const items = [{name: 'a', id: 1}];
* array2Map(items, 'id');
* // {1: {name: 'a', id: 1}}
*/
function array2Map<T extends Obj, K extends keyof T>(
items: T[],
key: K,
): Record<T[K], T>;
/**
* 将列表转化为 map
* @param items
* @param key 指定 item[key] 作为 map 的键
* @param value 指定 item[value] 作为 map 的值
* @example
* const items = [{name: 'a', id: 1}];
* array2Map(items, 'id', 'name');
* // {1: 'a'}
*/
function array2Map<T extends Obj, K extends keyof T, V extends keyof T>(
items: T[],
key: K,
value: V,
): Record<T[K], T[V]>;
/**
* 将列表转化为 map
* @param items
* @param key 指定 item[key] 作为 map 的键
* @param value 获取值
* @example
* const items = [{name: 'a', id: 1}];
* array2Map(items, 'id', (item) => `${item.id}-${item.name}`);
* // {1: '1-a'}
*/
function array2Map<T extends Obj, K extends keyof T, V>(
items: T[],
key: K,
value: (item: T) => V,
): Record<T[K], V>;
// endregion
/* eslint-disable @typescript-eslint/no-explicit-any */
/** 将列表转化为 map */
function array2Map<T extends Obj, K extends keyof T>(
items: T[],
key: K,
value: keyof T | ((item: T) => any) = item => item,
): Partial<Record<T[K], any>> {
return items.reduce((map, item) => {
const currKey = String(item[key]);
const currValue = isFunction(value) ? value(item) : item[value];
return { ...map, [currKey]: currValue };
}, {});
}
function mapAndFilter<I extends Obj = Obj>(
target: Array<I>,
options?: {
filter?: (item: I) => boolean;
},
): Array<I>;
function mapAndFilter<I extends Obj = Obj, T extends Obj = Obj>(
target: Array<I>,
options: {
filter?: (item: I) => boolean;
map: (item: I) => T;
},
): Array<T>;
/* eslint-enable @typescript-eslint/no-explicit-any */
function mapAndFilter<I = Obj, T = Obj>(
target: Array<I>,
options: {
filter?: (item: I) => boolean;
map?: (item: I) => T;
} = {},
) {
const { filter, map } = options;
return target.reduce((previousValue, currentValue) => {
const realValue = map ? map(currentValue) : currentValue;
const filtered = filter ? filter(currentValue) : true;
if (!filtered) {
// 如果filtered是false表示此项需要跳过
return previousValue;
}
// 如果filtered是true表示需要加上此项
return [...previousValue, realValue] as Array<I>;
}, [] as Array<I>);
}

View File

@@ -0,0 +1,66 @@
/*
* 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.
*/
type Timer = ReturnType<typeof setTimeout>;
type CachedKey = string | number;
export interface CachedData<TData = unknown> {
data: TData;
time: number;
}
export interface RecordData extends CachedData {
timer: Timer | undefined;
}
const cache = new Map<CachedKey, RecordData>();
const setCache = (
key: CachedKey,
cacheTime: number,
cachedData: CachedData,
) => {
const currentCache = cache.get(key);
if (currentCache?.timer) {
clearTimeout(currentCache.timer);
}
let timer: Timer | undefined = undefined;
if (cacheTime > -1) {
// if cache out, clear it
timer = setTimeout(() => {
cache.delete(key);
}, cacheTime);
}
cache.set(key, {
...cachedData,
timer,
});
};
const getCache = (key: CachedKey) => cache.get(key);
const clearCache = (key?: string | string[]) => {
if (key) {
const cacheKeys = Array.isArray(key) ? key : [key];
cacheKeys.forEach(cacheKey => cache.delete(cacheKey));
} else {
cache.clear();
}
};
export { getCache, setCache, clearCache };

View File

@@ -0,0 +1,129 @@
/*
* 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 dayjsUTC from 'dayjs/plugin/utc';
import dayjsTimezone from 'dayjs/plugin/timezone';
import dayjsDuration from 'dayjs/plugin/duration';
import dayjs, { type ManipulateType, type ConfigType, type Dayjs } from 'dayjs';
import { I18n } from '@coze-arch/i18n';
dayjs.extend(dayjsUTC);
dayjs.extend(dayjsTimezone);
dayjs.extend(dayjsDuration);
const FORMAT_DATE_MAP = {
Today: 'HH:mm',
CurrentYear: 'MM-DD HH:mm',
Default: 'YYYY-MM-DD HH:mm',
};
export const getFormatDateType = (time: number) => {
const compareTime = dayjs.unix(time);
const currentTime = dayjs();
if (compareTime.isSame(currentTime, 'day')) {
return FORMAT_DATE_MAP.Today;
}
if (compareTime.isSame(currentTime, 'year')) {
return FORMAT_DATE_MAP.CurrentYear;
}
return FORMAT_DATE_MAP.Default;
};
export const formatDate = (v: number, template = 'YYYY/MM/DD HH:mm:ss') =>
dayjs.unix(v).format(template);
export const CHINESE_TIMEZONE = 'Asia/Shanghai';
// 根据地区判断 海外返回UTC时间国内返回北京时间
export const getCurrentTZ = (param?: ConfigType): Dayjs => {
if (IS_OVERSEA) {
return dayjs(param).utc(true);
}
return dayjs(param).tz(CHINESE_TIMEZONE, true);
};
/**
* 获取dayjs add后的时间戳
*/
export const getTimestampByAdd = (value: number, unit?: ManipulateType) =>
dayjs().add(value, unit).unix();
/**
* 获取当前的时间戳
*/
export const getCurrentTimestamp = () => dayjs().unix();
/**
* 获取当前时间到次日UTC0点的时间间隔精确到分钟
* e.g. 12h 30m
*/
export const getRemainTime = () => {
const now = dayjs.utc();
const nextDay = now.add(1, 'day').startOf('day');
const diff = nextDay.diff(now);
const duration = dayjs.duration(diff);
const hour = duration.hours();
const minute = duration.minutes();
return `${hour}h ${minute}m`;
};
/**
* fork 自 packages/community/pages/src/bot/utils/index.ts
* 将11位的时间戳按以下格式显示
* 1. 不足一分钟, 显示”Just now“
* 2. 不足1小时, 显示”{n}min ago“例如 3min ago
* 3. 不足1天,显示”{n}h ago",例如 3h ago
* 4. 不足1个月,显示"{n}d ago", 例如 3d ago
* 5. 超过1个月,显示“{MM}/{DD}/{yyyy}”,例如12/1/2024中文是2024 年 12 月 1 日
*
*/
export const formatTimestamp = (timestampMs: number) => {
/** 秒级时间戳 */
const timestampSecond = Math.floor(timestampMs / 1000);
const now = Math.floor(Date.now() / 1000);
const diff = now - timestampSecond;
// 将时间差转换成分钟、小时和天数
const minutes = Math.floor(diff / 60);
const hours = Math.floor(diff / 3600);
const days = Math.floor(diff / 86400);
// 不足一分钟显示“Just now”
if (minutes < 1) {
return I18n.t('community_time_just_now');
}
// 不足一小时,显示“{n}min ago”
else if (hours < 1) {
return I18n.t('community_time_min', { n: minutes });
}
// 不足一天,显示“{n}h ago”
else if (days < 1) {
return I18n.t('community_time_hour', { n: hours });
}
// 不足一个月,显示“{n}d ago”
else if (days < 30) {
return I18n.t('community_time_day', { n: days });
}
// 超过一个月,显示“{MM}/{DD}/{yyyy}”
else {
const dayObj = dayjs(timestampSecond * 1000);
return I18n.t('community_time_date', {
yyyy: dayObj.get('y'),
mm: dayObj.get('M') + 1,
dd: dayObj.get('D'),
});
}
};

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.
*/
/**
* 获取最近一个可滚动元素
*/
export function closestScrollableElement(element: HTMLElement) {
const htmlElement = document.documentElement;
if (!element) {
return htmlElement;
}
let style = window.getComputedStyle(element);
const excludeStaticParent = style.position === 'absolute';
const overflowReg = /(auto|scroll|overlay)/;
if (style.position === 'fixed') {
return htmlElement;
}
let parent = element;
while (parent) {
style = window.getComputedStyle(parent);
if (excludeStaticParent && style.position === 'static') {
parent = parent.parentElement as HTMLElement;
continue;
}
if (
overflowReg.test(style.overflow + style.overflowY + style.overflowX) ||
parent.getAttribute('data-overflow') === 'true'
) {
return parent;
}
parent = parent.parentElement as HTMLElement;
}
return htmlElement;
}
// 解决浏览器拦截window.open行为接口catch则跳错误兜底页
export const openNewWindow = async (
callbackUrl: () => Promise<string> | string,
defaultUrl?: string,
) => {
const newWindow = window.open(defaultUrl || '');
let url = '';
try {
url = await callbackUrl();
} catch (error) {
url = `${location.origin}/404`;
newWindow?.close();
}
if (newWindow) {
newWindow.location = url;
}
};

View File

@@ -0,0 +1,159 @@
/*
* 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 EventEmitter from 'eventemitter3';
import { type AbilityKey } from '@coze-agent-ide/tool-config';
interface EventWithData<T extends EventEmitter.ValidEventTypes> {
event: EventEmitter.EventNames<T>;
args: Parameters<EventEmitter.EventListener<T, EventEmitter.EventNames<T>>>;
}
export class BufferedEventEmitter<T extends EventEmitter.ValidEventTypes> {
eventEmitter = new EventEmitter<T>();
started = true;
buffer: EventWithData<T>[] = [];
/**
* 触发事件
* @param event 事件名称
* @param args 参数
*/
emit<P extends EventEmitter.EventNames<T>>(
event: P,
...args: Parameters<EventEmitter.EventListener<T, P>>
) {
if (!this.started) {
this.buffer.push({
event,
args,
});
return;
}
this.eventEmitter.emit(event, ...args);
}
/**
* 订阅事件
* @param event 事件名称
* @param fn 事件回调
*/
on<P extends EventEmitter.EventNames<T>>(
event: P,
fn: EventEmitter.EventListener<T, P>,
) {
this.eventEmitter.on(event, fn);
}
/**
* 取消订阅事件
* @param event 事件名称
* @param fn 事件回调
*/
off<P extends EventEmitter.EventNames<T>>(
event: P,
fn: EventEmitter.EventListener<T, P>,
) {
this.eventEmitter.off(event, fn);
}
/**
* 开启缓存事件订阅器,开启时会将关闭时收到的事件对应的回调按顺序逐一触发
*/
start() {
this.started = true;
for (const { event, args } of this.buffer) {
this.emit(event, ...args);
}
}
/**
* 关闭缓存事件订阅器,在关闭时收到的事件会被缓存并延迟到下次开启时触发
*/
stop() {
this.started = false;
}
/**
* 清除缓存事件订阅器缓存的事件使得在重新开启start时不会触发在关闭stop时收到的事件对应的回调
*/
clear() {
this.buffer = [];
}
}
let eventEmitter: BufferedEventEmitter<EmitEventType> | null = null;
const initEventEmitter = () => {
if (!eventEmitter) {
eventEmitter = new BufferedEventEmitter<EmitEventType>();
}
};
// 模块折叠 有关事件
export enum OpenBlockEvent {
DATA_MEMORY_BLOCK_OPEN = 'dataMemoryBlockOpen',
TABLE_MEMORY_BLOCK_OPEN = 'tableMemoryBlockOpen',
DATA_SET_BLOCK_OPEN = 'dataSetBlockOpen',
TIME_CAPSULE_BLOCK_OPEN = 'timeCapsuleBlockOpen',
ONBORDING_MESSAGE_BLOCK_OPEN = 'onbordingMessageBlockOpen',
PLUGIN_API_BLOCK_OPEN = 'pluginApiBlockOpen',
WORKFLOW_BLOCK_OPEN = 'workflowBlockOpen',
IMAGEFLOW_BLOCK_OPEN = 'imageBlockOpen',
TASK_MANAGE_OPEN = 'taskManageOpen',
SUGGESTION_BLOCK_OPEN = 'suggestionBlockOpen',
TTS_BLOCK_OPEN = 'TTSBlockOpen',
FILEBOX_OPEN = 'FileboxOpen',
BACKGROUND_IMAGE_BLOCK = 'BackgroundImageOpen',
}
// 模块弹窗 有关事件
export enum OpenModalEvent {
PLUGIN_API_MODAL_OPEN = 'pluginApiModalOpen',
}
export type EmitEventType = OpenBlockEvent | OpenModalEvent | AbilityKey;
export const emitEvent = (event: EmitEventType, ...data: unknown[]) => {
initEventEmitter();
eventEmitter?.emit(event, ...data);
};
export const handleEvent = (
event: EmitEventType,
fn: (...args: unknown[]) => void,
) => {
initEventEmitter();
eventEmitter?.on(event, fn);
};
export const removeEvent = (
event: EmitEventType,
fn: (...args: unknown[]) => void,
) => {
initEventEmitter();
eventEmitter?.off(event, fn);
};
export enum DraftEvent {
DELETE_VARIABLE = 'deleteVariable',
}
export const draftEventEmitter = new EventEmitter();

View File

@@ -0,0 +1,56 @@
/*
* 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 { isObject } from 'lodash-es';
/**
* @param inputError 传啥都行,一般是 catch (e) 那个 e
* @param reason 解释
*/
export const getReportError = (
inputError: unknown,
reason?: string,
): {
error: Error;
meta: Record<string, unknown>;
} => {
if (inputError instanceof Error) {
return {
error: inputError,
meta: { reason },
};
}
if (!isObject(inputError)) {
return {
error: new Error(String(inputError)),
meta: { reason },
};
}
return {
error: new Error(''),
meta: { ...covertInputObject(inputError), reason },
};
};
const covertInputObject = (inputError: object) => {
if ('reason' in inputError) {
return {
...inputError,
reasonOfInputError: inputError.reason,
};
}
return inputError;
};

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,28 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactNode } from 'react';
import { isString } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
export const renderHtmlTitle = (prefix?: ReactNode) => {
const platformName = I18n.t('platform_name');
if (isString(prefix)) {
return `${prefix} - ${platformName}`;
}
return platformName;
};

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 const loadImage = (url: string): Promise<void> =>
new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve();
img.onerror = reject;
img.src = url;
});

View File

@@ -0,0 +1,84 @@
/*
* 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 { arrayBufferToObject } from './array-buffer-to-object';
export { isMobile } from './is-mobile';
export { safeJSONParse, typeSafeJSONParse } from './safe-json-parse';
export { type BytedUploader, upLoadFile } from './upload-file';
export { messageReportEvent, type MessageReportEvent } from './message-report';
export { ArrayUtil } from './array';
export { skillKeyToApiStatusKeyTransformer } from './skill';
export { loadImage } from './image';
export { renderHtmlTitle } from './html';
export { getParamsFromQuery, appendUrlParam, openUrl } from './url';
export { responsiveTableColumn } from './responsive-table-column';
export {
getFormatDateType,
formatDate,
getCurrentTZ,
getTimestampByAdd,
getCurrentTimestamp,
formatTimestamp,
} from './date';
export {
simpleformatNumber,
formatBytes,
formatNumber,
formatPercent,
formatTime,
getEllipsisCount,
exhaustiveCheck,
sleep,
} from './number';
export {
uploadFileV2,
type EventPayloadMaps,
type UploaderInstance,
type UploadFileV2Param,
type FileItem,
} from './upload-file-v2';
export { retryImport } from './retry-import';
export {
BufferedEventEmitter,
OpenBlockEvent,
OpenModalEvent,
EmitEventType,
emitEvent,
handleEvent,
removeEvent,
DraftEvent,
draftEventEmitter,
} from './event-handler';
export { setMobileBody, setPCBody } from './viewport';
/** 获取设备信息 */
export {
getIsIPhoneOrIPad,
getIsIPad,
getIsMobile,
getIsMobileOrIPad,
getIsSafari,
} from './platform';
export { closestScrollableElement, openNewWindow } from './dom';
export const timeoutPromise = (ms: number): Promise<void> =>
new Promise(resolve => {
setTimeout(resolve, ms);
});
export { getCache, setCache, clearCache } from './cache';

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.
*/
const MIN_SCREEN_WIDTH = 640;
export const isMobile = (): boolean => {
const width = document.documentElement.clientWidth;
return width <= MIN_SCREEN_WIDTH;
};

View File

@@ -0,0 +1,287 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { isObject } from 'lodash-es';
import { type ContentType, type Message } from '@coze-common/chat-core';
import { globalVars } from '@coze-arch/web-context';
import {
type ReportEvent,
REPORT_EVENTS as ReportEventNames,
createReportEvent,
} from '@coze-arch/report-events';
import { reporter } from '@coze-arch/logger';
import { CustomError } from '@coze-arch/bot-error';
// 这段代码是从 apps/bot/src/store/socket/utils.ts 复制出来的,后续也可以考虑统一
const hasSuggestion = (ext?: unknown) =>
isObject(ext) && 'has_suggest' in ext && ext.has_suggest === '1';
interface ErrorPayload {
reason: string;
error?: Error;
}
const overtime = 120000;
export class MessageReportEvent {
botID = '';
private _timer?: ReturnType<typeof setTimeout>;
private _receivingMessages = false;
private _receivingSuggests = false;
private _hasReceiveFirstChunk = false;
private _hasReceiveFirstSuggestChunk = false;
private _messageTotalContent = 0;
private _executeDraftBotEvent?: ReportEvent;
private _receiveMessagesEvent?: ReportEvent;
private _messageReceiveSuggestsEvent?: ReportEvent;
private _receiveTotalMessagesReportEvent?: ReportEvent;
getLogID() {
const logId = globalVars.LAST_EXECUTE_ID;
return { log_id: logId };
}
getMetaCtx() {
return {
bot_id: this.botID,
...this.getLogID(),
};
}
private _createExecuteDraftBotEvent = () =>
createReportEvent({
eventName: ReportEventNames.botDebugMessageSubmit,
meta: this.getMetaCtx(),
});
private _createReceiveMessagesEvent = () =>
createReportEvent({
eventName: ReportEventNames.receiveMessage,
meta: this.getMetaCtx(),
});
private _createMessageReceiveSuggestsEvent = () =>
createReportEvent({
eventName: ReportEventNames.messageReceiveSuggests,
meta: this.getMetaCtx(),
});
private _createReceiveTotalMessagesEvent = () =>
createReportEvent({
eventName: ReportEventNames.receiveTotalMessages,
meta: this.getMetaCtx(),
});
private _receiveMessagesEventGate = () => this._receivingMessages;
private _messageReceiveSuggestsEventGate = () => this._receivingSuggests;
private _clearTimeout() {
if (!this._timer) {
return;
}
clearTimeout(this._timer);
this._timer = void 0;
}
interrupt() {
this._clearTimeout();
if (this._receivingMessages || this._receivingSuggests) {
this._receiveTotalMessagesEvent.success();
if (this._receivingMessages) {
this.receiveMessageEvent.success();
}
if (this._receivingSuggests) {
this.messageReceiveSuggestsEvent.success();
}
}
}
private _receiveTotalMessagesEvent = {
start: () => {
// 打断了
this._receiveTotalMessagesReportEvent =
this._createReceiveTotalMessagesEvent();
},
error: (reason: string) => {
this._receiveTotalMessagesReportEvent?.addDurationPoint('failed');
this._receiveTotalMessagesReportEvent?.error({
reason,
});
},
success: (allFinish = false) => {
this._receiveTotalMessagesReportEvent?.addDurationPoint('success');
this._receiveTotalMessagesReportEvent?.success({
meta: {
reply_has_finished: allFinish,
},
});
},
finish: () => {
this._receiveTotalMessagesEvent?.success(true);
},
};
messageReceiveSuggestsEvent = {
start: () => {
this._messageReceiveSuggestsEvent =
this._createMessageReceiveSuggestsEvent();
this._receivingSuggests = true;
this._hasReceiveFirstSuggestChunk = false;
},
receiveSuggest: () => {
if (!this._messageReceiveSuggestsEventGate()) {
return;
}
if (!this._hasReceiveFirstSuggestChunk) {
this._messageReceiveSuggestsEvent?.addDurationPoint('first');
this._hasReceiveFirstSuggestChunk = true;
}
},
success: () => {
if (!this._messageReceiveSuggestsEventGate()) {
return;
}
this._messageReceiveSuggestsEvent?.addDurationPoint('success');
this._messageReceiveSuggestsEvent?.success({
meta: {
reply_has_finished: !this._receivingSuggests,
},
});
this._receivingSuggests = false;
},
finish: () => {
if (!this._messageReceiveSuggestsEventGate()) {
return;
}
this.messageReceiveSuggestsEvent.success();
this._receiveTotalMessagesEvent.finish();
},
error: ({ error, reason }: ErrorPayload) => {
if (!this._messageReceiveSuggestsEventGate()) {
return;
}
this._messageReceiveSuggestsEvent?.addDurationPoint('failed');
this._messageReceiveSuggestsEvent?.error({ error, reason });
this._receivingSuggests = false;
},
};
receiveMessageEvent = {
error: () => {
if (!this._receiveMessagesEventGate()) {
return;
}
this._receiveMessagesEvent?.addDurationPoint('failed');
this._receivingMessages = false;
},
success: (allFinish = false) => {
if (!this._receiveMessagesEventGate()) {
return;
}
this._receiveMessagesEvent?.addDurationPoint('success');
this._receiveMessagesEvent?.success({
meta: {
content_length: this._messageTotalContent,
reply_has_finished: allFinish,
},
});
this._receivingMessages = false;
},
start: () => {
this._receiveMessagesEvent = this._createReceiveMessagesEvent();
this._receivingMessages = true;
this._hasReceiveFirstChunk = false;
this._messageTotalContent = 0;
this._timer = setTimeout(this.receiveMessageEvent.error, overtime);
},
receiveMessage: (message: Message<ContentType>) => {
if (!this._receiveMessagesEventGate()) {
return;
}
if (!message.content) {
// 回复消息为空的错误事件上报
reporter.errorEvent({
eventName: ReportEventNames.emptyReceiveMessage,
error: new CustomError(
ReportEventNames.emptyReceiveMessage,
message.content || 'empty content',
),
});
}
this._messageTotalContent += message.content?.length ?? 0;
if (this._hasReceiveFirstChunk) {
return;
}
this._clearTimeout();
this._receiveMessagesEvent?.addDurationPoint('first');
this._hasReceiveFirstChunk = true;
},
finish: (message: Message<ContentType>) => {
if (!this._receiveMessagesEventGate()) {
return;
}
this.receiveMessageEvent.success(true);
if ('ext' in message && hasSuggestion(message.ext)) {
this.messageReceiveSuggestsEvent.start();
} else {
this._receiveTotalMessagesEvent.finish();
}
},
};
executeDraftBotEvent = {
start: () => {
this._executeDraftBotEvent = this._createExecuteDraftBotEvent();
this.interrupt();
},
success: () => {
this._executeDraftBotEvent?.addDurationPoint('finish');
this._executeDraftBotEvent?.success({
meta: {
...this.getLogID(),
},
});
this._receiveTotalMessagesEvent.start();
this.receiveMessageEvent.start();
},
error: ({ error, reason }: ErrorPayload) => {
this._executeDraftBotEvent?.error({
error,
reason,
meta: {
...this.getLogID(),
},
});
},
};
start(botID: string) {
this.botID = botID;
}
}
export const messageReportEvent = new MessageReportEvent();

View File

@@ -0,0 +1,116 @@
/*
* 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 { ceil } from 'lodash-es';
export const simpleformatNumber = (num: number | string) =>
new Intl.NumberFormat('en-US').format(parseInt(String(num)));
export const formatBytes = (bytes: number, decimals = 2) => {
if (!bytes) {
return '0 Byte';
}
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const digit = parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
return `${digit} ${sizes[i]}`;
};
const THOUSAND = 1e3;
const MILLION = 1e6;
const BILLION = 1e9;
const TRILLION = 1e12;
//将数字转换成K、M等单位
export const formatNumber = (num: number) => {
const absNum = Math.abs(num);
if (absNum >= TRILLION) {
return `${ceil(num / TRILLION, 1)}T`;
}
if (absNum >= BILLION) {
return `${ceil(num / BILLION, 1)}B`;
}
if (absNum >= MILLION) {
return `${ceil(num / MILLION, 1)}M`;
}
if (absNum >= THOUSAND) {
return `${ceil(num / THOUSAND, 1)}K`;
}
return num;
};
// 将数字转换成百分数, 向上取整
export const formatPercent = (num?: number): string => {
if (num === undefined || num === null) {
return 'NaN%';
}
const percentage = num * 100;
let formatted = percentage.toFixed(1);
// 如果小数点后一位是0则移除小数点和0
if (formatted.endsWith('.0')) {
formatted = formatted.slice(0, -2);
}
// 添加百分号并返回结果
return `${formatted}%`;
};
// 格式化时间, 毫秒, 保留一位小数点
// 比如6.7s, 3.2min, 100ms, 1.3h
export const formatTime = (ms: number) => {
const absMs = Math.abs(ms);
if (absMs >= 3600000) {
const hours = (ms / 3600000).toFixed(1);
return hours.endsWith('.0') ? `${hours.slice(0, -2)}h` : `${hours}h`;
}
if (absMs >= 60000) {
const minutes = (ms / 60000).toFixed(1);
return minutes.endsWith('.0')
? `${minutes.slice(0, -2)}min`
: `${minutes}min`;
}
if (absMs >= 10000) {
const seconds = (ms / 1000).toFixed(1);
return seconds.endsWith('.0') ? `${seconds.slice(0, -2)}s` : `${seconds}s`;
}
return `${ms.toFixed(0)}ms`;
};
export const getEllipsisCount = (num: number, max: number): string =>
num > max ? `${max}+` : `${num}`;
/**
* @deprecated 不知道这个函数是干啥的。。。
*/
export const exhaustiveCheck = (_v: never) => {
// empty
};
export async function sleep(timer = 3000) {
return new Promise<void>(resolve => {
setTimeout(() => resolve(), timer);
});
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Browser from 'bowser';
const browser = Browser.getParser(window.navigator.userAgent);
let getIsMobileCache: boolean | undefined;
/**
* 是否是移动设备
* 注ipad 不是移动设备
*/
const isMobile = () => browser.getPlatformType(true).includes('mobile');
export const getIsMobile = () => {
if (typeof getIsMobileCache === 'undefined') {
getIsMobileCache = isMobile();
}
return getIsMobileCache;
};
let getIsIPhoneOrIPadCache: boolean | undefined;
/**
* gpt-4 提供的代码
*/
export const getIsIPhoneOrIPad = () => {
if (typeof getIsIPhoneOrIPadCache === 'undefined') {
const { userAgent } = navigator;
const isAppleDevice = /iPad|iPhone|iPod/.test(userAgent);
const isIPadOS =
userAgent.includes('Macintosh') &&
'ontouchstart' in document.documentElement;
getIsIPhoneOrIPadCache = isAppleDevice || isIPadOS;
}
return getIsIPhoneOrIPadCache;
};
let getIsIPadCache: boolean | undefined;
/**
* gpt-4 提供的代码
*/
export const getIsIPad = () => {
if (typeof getIsIPadCache === 'undefined') {
const { userAgent } = navigator;
const isIPadDevice = /iPad/.test(userAgent);
const isIPadOS =
userAgent.includes('Macintosh') &&
'ontouchstart' in document.documentElement;
getIsIPadCache = isIPadDevice || isIPadOS;
}
return getIsIPadCache;
};
export const getIsMobileOrIPad = () => getIsMobile() || getIsIPhoneOrIPad();
export const getIsSafari = () => browser.getBrowserName(true) === 'safari';

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 EventEmitter from 'eventemitter3';
interface BusinessData<T> {
code: number; // 0: 成功, 其他: 错误码业务tidying
data?: T;
message?: string;
}
enum MessageType {
REQUEST = 'request',
RESPONSE = 'response',
}
interface MessageChannelEvent<T> {
syncNo: number;
type: MessageType;
senderName?: string;
toName?: string;
eventName?: string;
requestData?: unknown;
respondData?: BusinessData<T>;
}
enum ErrorType {
TIMEOUT = -1,
UNKNOWN = -2,
}
type DestoryListenerFun = () => void;
const DEFAULT_TIMEOUT = 3000;
export class PostMessageChannel {
private eventEmitter: EventEmitter = new EventEmitter();
private syncEventId: number = Math.ceil(10000 * Math.random());
private senderName = '';
private toName?: string = '';
private targetOrigin = '';
private channelPort: Window;
private onMessageFunc?: (event: MessageEvent) => void;
public constructor({
channelPort,
senderName,
toName,
targetOrigin = '*',
}: {
channelPort: Window;
senderName: string;
toName?: string;
targetOrigin?: string;
}) {
this.channelPort = channelPort;
this.senderName = senderName;
this.toName = toName;
this.targetOrigin = targetOrigin;
this.initListner();
}
public destory() {
this.onMessageFunc &&
window.removeEventListener('message', this.onMessageFunc);
this.eventEmitter.removeAllListeners();
}
public async send<T1, T2>(
eventName: string,
data: T1,
timeout = DEFAULT_TIMEOUT,
): Promise<BusinessData<T2>> {
const syncNo = this.syncEventId++;
const messageEvent: MessageChannelEvent<T1> = {
syncNo,
type: MessageType.REQUEST,
senderName: this.senderName,
toName: this.toName,
eventName,
requestData: data,
};
this.channelPort.postMessage(messageEvent, this.targetOrigin);
return await this.awaitRespond(syncNo, timeout);
}
public onRequest<T1, T2>(
eventName: string,
callback: (data: T1) => BusinessData<T2> | Promise<BusinessData<T2>>,
): DestoryListenerFun {
const onHandle = async (event: MessageEvent) => {
const messageEvent = event.data as MessageChannelEvent<unknown>;
const result = await callback(messageEvent.requestData as T1);
const responseMessageEvent: MessageChannelEvent<T2> = {
syncNo: messageEvent.syncNo,
type: MessageType.RESPONSE,
toName: messageEvent.senderName || '',
senderName: this.senderName,
eventName,
respondData: result,
};
if (event.source) {
// @ts-expect-error -- linter-disable-autofix
event.source.postMessage(responseMessageEvent, event.origin);
} else {
this.channelPort.postMessage(responseMessageEvent, this.targetOrigin);
}
};
this.eventEmitter.on(`${MessageType.REQUEST}_${eventName}`, onHandle);
return () => {
this.eventEmitter.off(`${MessageType.REQUEST}_${eventName}`, onHandle);
};
}
private initListner() {
this.onMessageFunc = (event: MessageEvent) => {
const messageEvent = event.data as MessageChannelEvent<unknown>;
if (
messageEvent.type === MessageType.RESPONSE &&
this.senderName === messageEvent.toName
) {
this.eventEmitter.emit(
`${MessageType.RESPONSE}_${messageEvent.syncNo}`,
messageEvent,
);
} else if (
messageEvent.type === MessageType.REQUEST &&
(!messageEvent.toName || this.senderName === messageEvent.toName)
) {
this.eventEmitter.emit(
`${MessageType.REQUEST}_${messageEvent.eventName}`,
event,
);
}
};
window.addEventListener('message', this.onMessageFunc);
}
private awaitRespond<T>(syncNo: number, timeout): Promise<BusinessData<T>> {
const eventName = `${MessageType.RESPONSE}_${syncNo}`;
return new Promise(resolve => {
const timeoutId = setTimeout(() => {
this.eventEmitter.emit(eventName, {
respondData: {
code: ErrorType.TIMEOUT,
message: 'timeout',
},
});
}, timeout);
this.eventEmitter.once(
eventName,
(messageEvent: MessageChannelEvent<T>) => {
clearTimeout(timeoutId);
resolve(
messageEvent.respondData || {
code: ErrorType.UNKNOWN,
message: 'unknow error',
},
);
},
);
});
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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 no-magic-numbers */
export enum WidthThresholds {
Small = 1280,
Medium = 1400,
Large = 1600,
Default = 1300,
}
export enum ColumnSize {
Small = 76,
Medium = 136,
Large = 160,
Default = 80,
}
type MinWidth = 'auto' | number;
interface WidthRange {
threshold: WidthThresholds;
columnWidth: ColumnSize;
}
const defaultRange = {
hreshold: WidthThresholds.Default,
columnWidth: ColumnSize.Default,
};
const colWidthRanges: WidthRange[] = [
{ threshold: WidthThresholds.Large, columnWidth: ColumnSize.Large },
{ threshold: WidthThresholds.Medium, columnWidth: ColumnSize.Medium },
{ threshold: WidthThresholds.Small, columnWidth: ColumnSize.Small },
];
export const responsiveTableColumn = (
width: number,
minWidth: MinWidth = ColumnSize.Medium,
): ColumnSize | string => {
if (minWidth === 'auto' || typeof minWidth !== 'number') {
return 'auto';
}
// 查找第一个符合条件的项
const range =
colWidthRanges.find(colWidth => width >= colWidth.threshold) ||
defaultRange;
// 返回 minWidth 或找到的 columnWidth取决于哪个更大
return Math.max(minWidth, range.columnWidth);
};

View File

@@ -0,0 +1,38 @@
/*
* 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.
*/
// TODO: https://github.com/web-infra-dev/rsbuild/issues/91
export const retryImport = <T>(
importFunction: () => Promise<T>,
maxRetryCount = 3,
) => {
let maxCount = 0;
const loadWithRetry = (): Promise<T> =>
new Promise((resolve, reject) => {
importFunction().then(
res => resolve(res),
error => {
if (maxCount >= maxRetryCount) {
reject(error);
} else {
maxCount++;
resolve(loadWithRetry());
}
},
);
});
return loadWithRetry();
};

View File

@@ -0,0 +1,53 @@
/*
* 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 } from '@coze-arch/report-events';
import { logger, reporter } from '@coze-arch/logger';
/**
* @deprecated 这其实是 unsafe 的,请换用 typeSafeJSONParse
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const safeJSONParse: (v: any, emptyValue?: any) => any = (
v,
emptyValue,
) => {
try {
const json = JSON.parse(v);
return json;
} catch (e) {
logger.persist.error({
error: e as Error,
eventName: REPORT_EVENTS.parseJSON,
message: 'parse json fail',
});
return emptyValue ?? void 0;
}
};
export const typeSafeJSONParse = (v: unknown): unknown => {
if (typeof v === 'object') {
return v;
}
try {
return JSON.parse(String(v));
} catch (e) {
reporter.errorEvent({
error: e as Error,
eventName: REPORT_EVENTS.parseJSON,
});
}
};

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type SkillKeyEnum } from '@coze-agent-ide/tool-config';
/**
* `能力模块主键` 转 `接口定义的属性名` 函数
* ⚠️ 命名需参看 @/services/auto-generate/developer_api/namespaces/developer_api > TabDisplayItems
*/
export const skillKeyToApiStatusKeyTransformer = ($key: SkillKeyEnum) =>
`${$key}_tab_status`;

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.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
getUploader as initUploader,
type CozeUploader,
type EventPayloadMaps,
} from '@coze-studio/uploader-adapter';
import { type GetUploadAuthTokenData } from '@coze-arch/bot-api/developer_api';
import { DeveloperApi } from '@coze-arch/bot-api';
import { getReportError } from './get-report-error';
export { type EventPayloadMaps };
export type UploaderInstance = CozeUploader;
const removeAllListeners = (instance: UploaderInstance) => {
instance.removeAllListeners('stream-progress');
instance.removeAllListeners('complete');
instance.removeAllListeners('error');
instance.removeAllListeners('progress');
};
export interface FileItem {
file: File;
/**
* 非图片的文件 type 为 object
* 这里显得很奇怪, 是为了对齐 @byted/uploader 的设计
*/
fileType: 'image' | 'object';
}
export interface UploadFileV2Param {
fileItemList: FileItem[];
userId: string;
signal: AbortSignal;
onProgress?: (event: EventPayloadMaps['progress']) => void;
onUploaderReady?: (uploader: UploaderInstance) => void;
onUploadError?: (event: EventPayloadMaps['error']) => void;
onGetTokenError?: (error: Error) => void;
onSuccess?: (event: EventPayloadMaps['complete']) => void;
onUploadAllSuccess?: (event: EventPayloadMaps['complete'][]) => void;
onStartUpload?: (param: (FileItem & { fileKey: string })[]) => void;
onGetUploadInstanceError?: (error: Error) => void;
timeout: number | undefined;
}
/**
* 改良版本的上传方法
* 1. 能够支持打断, 清除副作用
* 2. 更完善的回调函数
* 3. 支持一次上传多文件
*/
// eslint-disable-next-line max-lines-per-function -- 内部的方法分了模块但是都依赖同一个 context 作打断无法拆出去
export function uploadFileV2({
fileItemList,
userId,
signal,
onProgress,
onUploaderReady,
onUploadError,
onGetTokenError,
onSuccess,
onUploadAllSuccess,
onStartUpload,
timeout = 60000,
onGetUploadInstanceError,
}: UploadFileV2Param) {
return new Promise<void>(resolve => {
let bytedUploader: UploaderInstance | null = null;
let stopped = false;
signal?.addEventListener('abort', () => {
bytedUploader?.cancel();
if (bytedUploader) {
removeAllListeners(bytedUploader);
}
stopped = true;
resolve();
});
let list: EventPayloadMaps['complete'][] = [];
const getToken = async () => {
try {
const dataAuth = await DeveloperApi.GetUploadAuthToken(
{
scene: 'bot_task',
},
{ timeout },
);
const result = dataAuth.data;
if (!result) {
throw new Error('Invalid GetUploadAuthToken Response');
}
return result;
} catch (e) {
onGetTokenError?.(getReportError(e).error);
throw e;
}
};
const upload = (authToken: GetUploadAuthTokenData) => {
const { service_id, upload_host, auth, schema } =
authToken as GetUploadAuthTokenData & { schema?: string };
const uploader = initUploader(
{
schema,
useFileExtension: true,
// 解决报错问题:
userId,
appId: APP_ID,
// cp-disable-next-line
imageHost: `https://${upload_host}`, //imageX上传必填
imageConfig: {
serviceId: service_id || '', // 在视频云中申请的服务id
},
objectConfig: {
serviceId: service_id || '',
},
imageFallbackHost: IMAGE_FALLBACK_HOST,
region: BYTE_UPLOADER_REGION,
uploadTimeout: timeout,
},
IS_OVERSEA,
);
bytedUploader = uploader;
onUploaderReady?.(uploader);
const fileAndKeyList = fileItemList.map(({ file, fileType }) => {
const fileKey = uploader.addFile({
file,
stsToken: {
CurrentTime: auth?.current_time || '',
ExpiredTime: auth?.expired_time || '',
SessionToken: auth?.session_token || '',
AccessKeyId: auth?.access_key_id || '',
SecretAccessKey: auth?.secret_access_key || '',
},
type: fileType, // 上传文件类型三个可选值video(视频或者音频,默认值)image(图片)object普通文件
});
return { file, fileType, fileKey };
});
onStartUpload?.(fileAndKeyList);
fileAndKeyList.forEach(fileAndKey => {
uploader.start(fileAndKey.fileKey);
});
uploader.on('complete', inform => {
onSuccess?.(inform as any);
list.push(inform as any);
if (list.length === fileAndKeyList.length) {
// 按顺序赋值
// @ts-expect-error -- linter-disable-autofix
list = fileAndKeyList.map(({ fileKey }) =>
list.find(v => v.key === fileKey),
);
onUploadAllSuccess?.(list);
}
});
uploader.on('error', inform => {
onUploadError?.(inform as any);
});
uploader.on('progress', inform => {
onProgress?.(inform as any);
});
};
const start = async () => {
const [authData] = await Promise.all([getToken()]);
if (stopped) {
return;
}
upload(authData);
};
start();
});
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/naming-convention */
import { userStoreService } from '@coze-studio/user-store';
import {
getUploader as initUploader,
type CozeUploader,
type Config as BytedUploaderConfig,
} from '@coze-studio/uploader-adapter';
import { type developer_api } from '@coze-arch/bot-api/developer_api';
import { DeveloperApi, workflowApi } from '@coze-arch/bot-api';
export type BytedUploader = CozeUploader;
interface Inform {
uploadResult: {
Uri: string;
};
extra: string;
percent?: number;
}
type BizConfig = Record<
string,
{
getAuthToken: () => Promise<{
serviceId: string;
uploadHost: string;
stsToken: BytedUploaderConfig['stsToken'];
schema: string;
}>;
}
>;
const bizConfig: BizConfig = {
bot: {
getAuthToken: async () => {
const dataAuth = await DeveloperApi.GetUploadAuthToken({
scene: 'bot_task',
});
const dataAuthnr = dataAuth.data;
const { service_id, upload_host, auth, schema } = (dataAuthnr ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{}) as any;
return {
schema,
serviceId: service_id || '',
uploadHost: upload_host || '',
stsToken: {
CurrentTime: auth?.current_time || '',
ExpiredTime: auth?.expired_time || '',
SessionToken: auth?.session_token || '',
AccessKeyId: auth?.access_key_id || '',
SecretAccessKey: auth?.secret_access_key || '',
},
};
},
},
workflow: {
getAuthToken: async () => {
const dataAuth = await workflowApi.GetUploadAuthToken({
scene: 'imageflow',
});
const dataAuthnr = dataAuth.data;
const { service_id, upload_host, auth, schema } = (dataAuthnr ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{}) as any;
return {
schema,
serviceId: service_id || '',
uploadHost: upload_host || '',
stsToken: {
CurrentTime: auth?.current_time || '',
ExpiredTime: auth?.expired_time || '',
SessionToken: auth?.session_token || '',
AccessKeyId: auth?.access_key_id || '',
SecretAccessKey: auth?.secret_access_key || '',
},
};
},
},
};
export function upLoadFile({
biz = 'bot',
file,
fileType = 'image',
getProgress,
getUploader,
getUploadAuthToken,
}: {
/** 业务, 不同业务对应不同的 ImageX 服务 */
biz?: 'bot' | 'workflow' | string;
file: File;
fileType?: 'image' | 'object';
getProgress?: (progress: number) => void;
getUploader?: (uploader: BytedUploader) => void;
// 业务方自己获取upload token
getUploadAuthToken?: () => Promise<developer_api.GetUploadAuthTokenResponse>;
}) {
const config = bizConfig[biz];
if (!config && !getUploadAuthToken) {
throw new Error('upLoadFile need biz');
}
const result = new Promise<string>((resolve, reject) => {
// eslint-disable-next-line complexity
(async function () {
try {
let serviceId, uploadHost, stsToken, schema;
if (config) {
const data = await config.getAuthToken();
serviceId = data.serviceId;
uploadHost = data.uploadHost;
stsToken = data.stsToken;
schema = data.schema;
} else if (getUploadAuthToken) {
const { data } = await getUploadAuthToken();
// @ts-expect-error -- linter-disable-autofix
serviceId = data.service_id;
// @ts-expect-error -- linter-disable-autofix
uploadHost = data.upload_host;
// @ts-expect-error -- linter-disable-autofix
schema = data.schema;
// cp-disable-next-line
if (uploadHost.startsWith('https://')) {
uploadHost = uploadHost.substr(8);
}
stsToken = {
// @ts-expect-error -- linter-disable-autofix
CurrentTime: data.auth?.current_time || '',
// @ts-expect-error -- linter-disable-autofix
ExpiredTime: data.auth?.expired_time || '',
// @ts-expect-error -- linter-disable-autofix
SessionToken: data.auth?.session_token || '',
// @ts-expect-error -- linter-disable-autofix
AccessKeyId: data.auth?.access_key_id || '',
// @ts-expect-error -- linter-disable-autofix
SecretAccessKey: data.auth?.secret_access_key || '',
};
}
const bytedUploader: BytedUploader = initUploader(
{
schema,
useFileExtension: true,
userId: userStoreService.getUserInfo()?.user_id_str || '',
appId: APP_ID,
// cp-disable-next-line
imageHost: `https://${uploadHost}`, //imageX上传必填
imageConfig: {
serviceId: serviceId || '', // 在视频云中申请的服务id
},
objectConfig: {
serviceId: serviceId || '',
},
imageFallbackHost: IMAGE_FALLBACK_HOST,
region: BYTE_UPLOADER_REGION,
},
IS_OVERSEA,
);
getUploader?.(bytedUploader);
bytedUploader.on('complete', inform => {
const { uploadResult } = inform;
resolve(uploadResult.Uri ?? '');
});
bytedUploader.on('error', inform => {
const { extra } = inform;
reject(extra);
});
if (getProgress) {
bytedUploader.on('progress', inform => {
const { percent } = inform as unknown as Inform;
getProgress(percent || 0);
});
}
const fileKey = bytedUploader.addFile({
file,
stsToken,
type: fileType, // 上传文件类型三个可选值video(视频或者音频,默认值)image(图片)object普通文件
});
bytedUploader.start(fileKey);
} catch (e) {
reject(e);
}
})();
});
return result;
}

View File

@@ -0,0 +1,49 @@
/*
* 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 queryString from 'query-string';
import { getIsMobile, getIsSafari } from './platform';
export const getParamsFromQuery = (params: { key: string }) => {
const { key = '' } = params;
const queryParams = queryString.parse(location.search);
return (queryParams?.[key] ?? '') as string;
};
export function appendUrlParam(
url: string,
key: string,
value: string | string[] | null | undefined,
) {
const urlInfo = queryString.parseUrl(url);
if (!value) {
delete urlInfo.query[key];
} else {
urlInfo.query[key] = value;
}
return queryString.stringifyUrl(urlInfo);
}
export function openUrl(url?: string) {
if (!url) {
return;
}
if (getIsMobile() && getIsSafari()) {
location.href = url;
} else {
window.open(url, '_blank');
}
}

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.
*/
export const setMobileBody = () => {
const bodyStyle = document?.body?.style;
const htmlStyle = document?.getElementsByTagName('html')?.[0]?.style;
if (bodyStyle && htmlStyle) {
bodyStyle.minHeight = '0';
htmlStyle.minHeight = '0';
bodyStyle.minWidth = '0';
htmlStyle.minWidth = '0';
}
};
export const setPCBody = () => {
const bodyStyle = document?.body?.style;
const htmlStyle = document?.getElementsByTagName('html')?.[0]?.style;
if (bodyStyle && htmlStyle) {
bodyStyle.minHeight = '600px';
htmlStyle.minHeight = '600px';
bodyStyle.minWidth = '1200px';
htmlStyle.minWidth = '1200px';
}
};