feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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 {};
|
||||
}
|
||||
}
|
||||
120
frontend/packages/arch/bot-utils/src/array.ts
Normal file
120
frontend/packages/arch/bot-utils/src/array.ts
Normal 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>);
|
||||
}
|
||||
66
frontend/packages/arch/bot-utils/src/cache.ts
Normal file
66
frontend/packages/arch/bot-utils/src/cache.ts
Normal 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 };
|
||||
129
frontend/packages/arch/bot-utils/src/date.ts
Normal file
129
frontend/packages/arch/bot-utils/src/date.ts
Normal 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'),
|
||||
});
|
||||
}
|
||||
};
|
||||
68
frontend/packages/arch/bot-utils/src/dom.ts
Normal file
68
frontend/packages/arch/bot-utils/src/dom.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
159
frontend/packages/arch/bot-utils/src/event-handler.ts
Normal file
159
frontend/packages/arch/bot-utils/src/event-handler.ts
Normal 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();
|
||||
56
frontend/packages/arch/bot-utils/src/get-report-error.ts
Normal file
56
frontend/packages/arch/bot-utils/src/get-report-error.ts
Normal 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;
|
||||
};
|
||||
17
frontend/packages/arch/bot-utils/src/global.d.ts
vendored
Normal file
17
frontend/packages/arch/bot-utils/src/global.d.ts
vendored
Normal 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' />
|
||||
28
frontend/packages/arch/bot-utils/src/html.ts
Normal file
28
frontend/packages/arch/bot-utils/src/html.ts
Normal 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;
|
||||
};
|
||||
23
frontend/packages/arch/bot-utils/src/image.ts
Normal file
23
frontend/packages/arch/bot-utils/src/image.ts
Normal 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;
|
||||
});
|
||||
84
frontend/packages/arch/bot-utils/src/index.ts
Normal file
84
frontend/packages/arch/bot-utils/src/index.ts
Normal 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';
|
||||
22
frontend/packages/arch/bot-utils/src/is-mobile.ts
Normal file
22
frontend/packages/arch/bot-utils/src/is-mobile.ts
Normal 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;
|
||||
};
|
||||
287
frontend/packages/arch/bot-utils/src/message-report.ts
Normal file
287
frontend/packages/arch/bot-utils/src/message-report.ts
Normal 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();
|
||||
116
frontend/packages/arch/bot-utils/src/number.ts
Normal file
116
frontend/packages/arch/bot-utils/src/number.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
73
frontend/packages/arch/bot-utils/src/platform.ts
Normal file
73
frontend/packages/arch/bot-utils/src/platform.ts
Normal 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';
|
||||
174
frontend/packages/arch/bot-utils/src/post-message-channel.ts
Normal file
174
frontend/packages/arch/bot-utils/src/post-message-channel.ts
Normal 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',
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
38
frontend/packages/arch/bot-utils/src/retry-import.ts
Normal file
38
frontend/packages/arch/bot-utils/src/retry-import.ts
Normal 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();
|
||||
};
|
||||
53
frontend/packages/arch/bot-utils/src/safe-json-parse.ts
Normal file
53
frontend/packages/arch/bot-utils/src/safe-json-parse.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
24
frontend/packages/arch/bot-utils/src/skill.ts
Normal file
24
frontend/packages/arch/bot-utils/src/skill.ts
Normal 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`;
|
||||
201
frontend/packages/arch/bot-utils/src/upload-file-v2.ts
Normal file
201
frontend/packages/arch/bot-utils/src/upload-file-v2.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
208
frontend/packages/arch/bot-utils/src/upload-file.ts
Normal file
208
frontend/packages/arch/bot-utils/src/upload-file.ts
Normal 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;
|
||||
}
|
||||
49
frontend/packages/arch/bot-utils/src/url.ts
Normal file
49
frontend/packages/arch/bot-utils/src/url.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
37
frontend/packages/arch/bot-utils/src/viewport.ts
Normal file
37
frontend/packages/arch/bot-utils/src/viewport.ts
Normal 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';
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user