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,77 @@
# @coze-arch/bot-utils
common utils extracts from apps/bot
## Overview
This package is part of the Coze Studio monorepo and provides utilities functionality. It includes modal, api.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-arch/bot-utils": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-arch/bot-utils';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Modal
- Api
## API Reference
### Exports
- `arrayBufferToObject`
- `isMobile`
- `safeJSONParse, typeSafeJSONParse`
- `type BytedUploader, upLoadFile`
- `messageReportEvent, type MessageReportEvent`
- `ArrayUtil`
- `skillKeyToApiStatusKeyTransformer`
- `loadImage`
- `renderHtmlTitle`
- `getParamsFromQuery, appendUrlParam, openUrl`
*And more...*
For detailed API documentation, please refer to the TypeScript definitions.
## Development
This package is built with:
- TypeScript
- Modern JavaScript
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,70 @@
/*
* 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 { ArrayUtil } from '../src/array';
describe('array', () => {
it('array2Map', () => {
const testItem1 = { id: '1', name: 'Alice', age: 20 };
const testItem2 = { id: '2', name: 'Bob', age: 25 };
const { array2Map } = ArrayUtil;
const array1 = [testItem1, testItem2];
const mapById = array2Map(array1, 'id');
expect(mapById).toEqual({
'1': testItem1,
'2': testItem2,
});
const mapByName = array2Map(array1, 'name', 'age');
expect(mapByName).toEqual({ Alice: 20, Bob: 25 });
const array = [testItem1, testItem2];
const mapByIdFunc = array2Map(
array,
'id',
item => `${item.name}-${item.age}`,
);
expect(mapByIdFunc).toEqual({ '1': 'Alice-20', '2': 'Bob-25' });
});
it('mapAndFilter', () => {
const { mapAndFilter } = ArrayUtil;
const array = [
{ id: 1, name: 'Alice', value: 100 },
{ id: 2, name: 'Bob', value: 200 },
];
// filter
const result1 = mapAndFilter(array, {
filter: item => item.name === 'Alice',
});
expect(result1).toEqual([{ id: 1, name: 'Alice', value: 100 }]);
// map
const result2 = mapAndFilter(array, {
map: item => ({ value: item.value }),
});
expect(result2).toEqual([{ value: 100 }, { value: 200 }]);
// filter & map
const result3 = mapAndFilter(array, {
filter: item => item.value > 100,
map: item => ({ id: item.id, name: item.name }),
});
expect(result3).toEqual([{ id: 2, name: 'Bob' }]);
});
});

View File

@@ -0,0 +1,114 @@
/*
* 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 dayjs from 'dayjs';
import { I18n } from '@coze-arch/i18n';
import {
getCurrentTZ,
getFormatDateType,
formatDate,
getRemainTime,
getTimestampByAdd,
formatTimestamp,
} from '../src/date';
vi.mock('@coze-arch/i18n');
vi.spyOn(I18n, 't');
describe('Date', () => {
it('#getFormatDateType', () => {
const now = dayjs();
expect(getFormatDateType(now.unix())).toEqual('HH:mm');
expect(
getFormatDateType(
dayjs(now)
.date(now.date() === 1 ? 2 : 1)
.unix(),
),
).toEqual('MM-DD HH:mm');
expect(getFormatDateType(dayjs(now).add(1, 'year').unix())).toEqual(
'YYYY-MM-DD HH:mm',
);
});
it('#dayjsForRegion Oversea should return UTC format', () => {
vi.stubGlobal('IS_OVERSEA', true);
expect(getCurrentTZ().isUTC()).toBe(true);
expect(getCurrentTZ().utcOffset()).toBe(0);
});
it('#dayjsForRegion China should return UTC+8 format', () => {
vi.stubGlobal('IS_OVERSEA', false);
expect(getCurrentTZ().isUTC()).toBe(false);
expect(getCurrentTZ().utcOffset()).toBe(60 * 8);
});
it('#formatDate', () => {
const date = formatDate(1718782764);
expect(date).toBe('2024/06/19 15:39:24');
});
it('#getRemainTime', () => {
vi.useFakeTimers();
const date = new Date('2024-08-19T15:30:00+08:00');
vi.setSystemTime(date);
expect(getRemainTime()).toBe('16h 30m');
vi.useRealTimers();
});
it('#dayjsAdd', () => {
vi.useFakeTimers();
const date = new Date('2024-08-19T15:30:00+08:00');
vi.setSystemTime(date);
const unix = getTimestampByAdd(1, 'd');
expect(unix).toBe(dayjs('2024-08-20T15:30:00+08:00').unix());
vi.useRealTimers();
});
});
describe('format timestamp', () => {
beforeEach(() => {
const MOCK_NOW = dayjs('2024-09-24 20:00:00');
vi.setSystemTime(MOCK_NOW);
});
it('just now', () => {
formatTimestamp(dayjs('2024-09-24 19:59:01').valueOf());
expect(I18n.t).toHaveBeenCalledWith('community_time_just_now');
});
it('n min', () => {
formatTimestamp(dayjs('2024-09-24 19:58:01').valueOf());
expect(I18n.t).toHaveBeenCalledWith('community_time_min', { n: 1 });
});
it('n hours', () => {
formatTimestamp(dayjs('2024-09-24 17:58:01').valueOf());
expect(I18n.t).toHaveBeenCalledWith('community_time_hour', { n: 2 });
});
it('n days', () => {
formatTimestamp(dayjs('2024-09-21 17:58:01').valueOf());
expect(I18n.t).toHaveBeenCalledWith('community_time_day', { n: 3 });
});
it('full date', () => {
formatTimestamp(dayjs('2024-07-21 17:58:01').valueOf());
expect(I18n.t).toHaveBeenCalledWith('community_time_date', {
yyyy: 2024,
mm: 7,
dd: 21,
});
});
});

View File

@@ -0,0 +1,45 @@
/*
* 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 { openNewWindow } from '../src/dom';
it('openNewWindow', async () => {
const testUrl = 'test_url';
const testOrigin = 'test_origin';
const newWindow = {
close: vi.fn(),
location: '',
};
vi.stubGlobal('window', {
open: vi.fn(() => newWindow),
});
vi.stubGlobal('location', {
origin: testOrigin,
});
const cb = vi.fn(() => Promise.resolve(testUrl));
const cbWithError = vi.fn(() => Promise.reject(new Error()));
await openNewWindow(cb);
expect(newWindow.close).not.toHaveBeenCalled();
expect(newWindow.location).equal(testUrl);
await openNewWindow(cbWithError);
expect(newWindow.close).toHaveBeenCalled();
expect(newWindow.location).equal(`${testOrigin}/404`);
vi.clearAllMocks();
});

View File

@@ -0,0 +1,48 @@
/*
* 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 { getReportError } from '../src/get-report-error';
describe('getReportError', () => {
afterEach(() => {
vi.clearAllMocks();
});
test('common error', () => {
const error = new Error('123');
const result = getReportError(error, 'testError');
expect(result).toMatchObject({ error, meta: { reason: 'testError' } });
});
test('stringify error', () => {
const result = getReportError('123', 'testError');
expect(result).toMatchObject({
error: new Error('123'),
meta: { reason: 'testError' },
});
});
test('object error', () => {
const result = getReportError(
{ foo: 'bar', reason: 'i am fool' },
'testError',
);
expect(result).toMatchObject({
error: new Error(''),
meta: { reason: 'testError', reasonOfInputError: 'i am fool' },
});
});
});

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 { type ReactNode } from 'react';
import { renderHtmlTitle } from '../src/html';
vi.mock('@coze-arch/i18n', () => ({
I18n: { t: vi.fn(k => k) },
}));
describe('html', () => {
test('renderHtmlTitle', () => {
expect(renderHtmlTitle('test')).equal('test - platform_name');
expect(renderHtmlTitle({} as unknown as ReactNode)).equal('platform_name');
});
});

View File

@@ -0,0 +1,47 @@
/*
* 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 { loadImage } from '../src/image';
describe('image', () => {
test('loadImage with success', async () => {
vi.stubGlobal(
'Image',
class Image {
onload!: () => void;
set src(url: string) {
this.onload();
}
},
);
await expect(loadImage('test')).resolves.toBeUndefined();
vi.clearAllMocks();
});
test('loadImage with fail', async () => {
vi.stubGlobal(
'Image',
class Image {
onerror!: () => void;
set src(url: string) {
this.onerror();
}
},
);
await expect(loadImage('test')).rejects.toBeUndefined();
vi.clearAllMocks();
});
});

View File

@@ -0,0 +1,21 @@
/*
* 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.
*/
describe('Hello World', () => {
it('test', () => {
expect(1).toBe(1);
});
});

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.
*/
import { isMobile } from '../src/is-mobile';
describe('is-mobile', () => {
const TARGET_WIDTH = 640;
test('isMobile with false', () => {
vi.stubGlobal('document', {
documentElement: {
clientWidth: TARGET_WIDTH + 10,
},
});
expect(isMobile()).toBeFalsy();
});
test('isMobile with true', () => {
vi.stubGlobal('document', {
documentElement: {
clientWidth: TARGET_WIDTH - 10,
},
});
expect(isMobile()).toBeTruthy();
});
});

View File

@@ -0,0 +1,155 @@
/*
* 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 { createReportEvent } from '@coze-arch/report-events';
import { messageReportEvent } from '../src/message-report';
const TEST_LOG_ID = 'test_log_id';
const TEST_BOT_ID = 'test_bot_id';
vi.mock('@coze-arch/web-context', () => ({
globalVars: {
LAST_EXECUTE_ID: 'test_log_id',
},
}));
const mockAddDurationPoint = vi.fn();
const mockSuccess = vi.fn();
const mockError = vi.fn();
vi.mock('@coze-arch/report-events', async () => {
const actual: Record<string, unknown> = await vi.importActual(
'@coze-arch/report-events',
);
return {
...actual,
createReportEvent: vi.fn(() => ({
addDurationPoint: mockAddDurationPoint,
success: mockSuccess,
error: mockError,
})),
};
});
vi.mock('@coze-arch/logger', () => ({
reporter: vi.fn(),
}));
vi.mock('@coze-arch/bot-error', () => ({}));
describe('message-report', () => {
afterEach(() => {
vi.clearAllMocks();
});
test('Should setup correctly', () => {
const { log_id } = messageReportEvent.getLogID();
expect(log_id).equal(TEST_LOG_ID);
messageReportEvent.start(TEST_BOT_ID);
const { bot_id, log_id: logId } = messageReportEvent.getMetaCtx();
expect(bot_id).equal(TEST_BOT_ID);
expect(logId).equal(TEST_LOG_ID);
});
/// messageReceiveSuggestsEvent & receiveMessageEvent
test('messageReceiveSuggestsEvent & receiveMessageEvent should not trigger report event if `start` has not been called', () => {
[
messageReportEvent.messageReceiveSuggestsEvent,
messageReportEvent.receiveMessageEvent,
].forEach(event => {
event.success();
event.finish('' as any);
event.error({
error: new Error(),
reason: '',
});
if (event === messageReportEvent.receiveMessageEvent) {
event.receiveMessage({ message_id: '' });
}
expect(createReportEvent).not.toHaveBeenCalled();
expect(mockAddDurationPoint).not.toHaveBeenCalled();
});
});
test('messageReceiveSuggestsEvent & receiveMessageEvent should trigger reporter correctly by calling `receiveSuggest`', () => {
messageReportEvent.messageReceiveSuggestsEvent.start();
messageReportEvent.messageReceiveSuggestsEvent.receiveSuggest();
expect(createReportEvent).toHaveBeenCalled();
expect(mockAddDurationPoint).toHaveBeenCalledWith('first');
});
test('`success` should trigger reporter correctly', () => {
[
messageReportEvent.messageReceiveSuggestsEvent,
messageReportEvent.messageReceiveSuggestsEvent,
].forEach(event => {
['success', 'finish'].forEach(tag => {
event.start();
event[tag]();
expect(createReportEvent).toHaveBeenCalled();
expect(mockAddDurationPoint).toHaveBeenCalledWith('success');
expect(mockSuccess).toHaveBeenCalled();
});
});
});
test('messageReceiveSuggestsEvent & receiveMessageEvent should trigger reporter correctly by calling `error`', () => {
[
messageReportEvent.messageReceiveSuggestsEvent,
messageReportEvent.messageReceiveSuggestsEvent,
].forEach(event => {
event.start();
event.error({
error: new Error(),
reason: '',
});
expect(createReportEvent).toHaveBeenCalled();
expect(mockAddDurationPoint).toHaveBeenCalledWith('failed');
expect(mockError).toHaveBeenCalled();
});
});
test('executeDraftBotEvent should report correctly by calling start', () => {
const event = messageReportEvent.executeDraftBotEvent;
event.start();
event.success();
expect(createReportEvent).toHaveBeenCalled();
expect(mockAddDurationPoint).toHaveBeenCalledWith('finish');
expect(mockSuccess).toHaveBeenCalled();
});
test('executeDraftBotEvent should report correctly by calling error', () => {
const event = messageReportEvent.executeDraftBotEvent;
event.start();
event.error({ error: new Error(), reason: '' });
expect(createReportEvent).toHaveBeenCalled();
expect(mockError).toHaveBeenCalled();
});
test('interrupt', () => {
[
messageReportEvent.messageReceiveSuggestsEvent,
messageReportEvent.messageReceiveSuggestsEvent,
].forEach((event, index) => {
if (index === 0) {
event.receiveSuggest();
}
event.start();
messageReportEvent.interrupt();
expect(mockSuccess).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,123 @@
/*
* 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';
import {
formatBytes,
formatNumber,
getEllipsisCount,
simpleformatNumber,
sleep,
formatPercent,
formatTime,
} from '../src/number';
describe('Number', () => {
it('#simpleformatNumber', () => {
expect(simpleformatNumber('100')).toEqual('100');
expect(simpleformatNumber('100.1')).toEqual('100');
expect(simpleformatNumber(100.1)).toEqual('100');
expect(simpleformatNumber(1100)).toEqual('1,100');
expect(simpleformatNumber('1100')).toEqual('1,100');
});
it('formatBytes', () => {
const k = 1024;
const decimals = 2;
const genRandomNum = (bytes: number) =>
Array(bytes)
.fill(0)
.reduce(
(prev, _, idx) =>
prev +
Math.floor(((Math.random() + 1) * (k - 1) * Math.pow(k, idx)) / 2),
0,
);
const calDigit = (num: number, unit: number) =>
parseFloat((num / Math.pow(k, unit)).toFixed(decimals));
expect(formatBytes(0)).equal('0 Byte');
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
sizes.forEach((size, idx) => {
const num = genRandomNum(idx + 1);
const digit = calDigit(num, idx);
expect(formatBytes(num)).equal(`${digit} ${size}`);
});
});
it('formatNumber', () => {
const base = 1000;
const units = ['', 'K', 'M', 'B', 'T'];
const genRandomNum = (order: number) =>
Array(order)
.fill(0)
.reduce(
(prev, _, idx) =>
prev +
Math.floor(
((Math.random() + 1) * (base - 1) * Math.pow(base, idx)) / 2,
),
0,
);
const calDigit = (num: number, unit: number) =>
ceil(Math.abs(num) / Math.pow(base, unit), 1);
units.forEach((unit, idx) => {
const num = genRandomNum(idx + 1);
const digit = calDigit(num, idx);
expect(formatNumber(num).toString()).equal(`${digit}${unit}`);
});
});
it('getEllipsisCount', () => {
const max = 1000;
const num1 = max - 1;
const num2 = max;
const num3 = max + 1;
expect(getEllipsisCount(num1, max)).equal(`${num1}`);
expect(getEllipsisCount(num2, max)).equal(`${num2}`);
expect(getEllipsisCount(num3, max)).equal(`${max}+`);
});
it('sleep', async () => {
const mockFn = vi.fn();
const interval = 3000;
vi.useFakeTimers();
const promisedSleep = sleep(interval).then(() => {
mockFn();
});
vi.advanceTimersByTime(interval);
await promisedSleep;
expect(mockFn).toHaveBeenCalled();
vi.restoreAllMocks();
});
it('formatPercent', () => {
expect(formatPercent(0.1)).equal('10%');
expect(formatPercent(0.123456)).equal('12.3%');
expect(formatPercent(0.12556)).equal('12.6%');
expect(formatPercent(1)).equal('100%');
expect(formatPercent()).equal('NaN%');
});
it('formatTime', () => {
expect(formatTime(1000)).equal('1000ms');
expect(formatTime(12000)).equal('12s');
expect(formatTime(12330)).equal('12.3s');
expect(formatTime(1000.12332)).equal('1000ms');
});
});

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.
*/
/*
表格单元格宽度适配
当 width 小于 WidthThresholds.Small 时应该返回 ColumnSize.Default。
当 width 大于等于 WidthThresholds.Small 但小于 WidthThresholds.Medium 时应该返回 ColumnSize.Small。
当 width 大于等于 WidthThresholds.Medium 但小于 WidthThresholds.Large 时应该返回 ColumnSize.Medium。
当 width 大于等于 WidthThresholds.Large 时应该返回 ColumnSize.Large。
当 minWidth 为 'auto' 时应该返回 'auto'。
当 minWidth 是一个指定数字时,应该返回 minWidth 和 columnWidth 中较大的一个。*/
import { describe, it, expect } from 'vitest';
import {
responsiveTableColumn,
ColumnSize,
WidthThresholds,
} from '../src/responsive-table-column';
describe('responsiveTableColumn', () => {
it('returns auto for minWidth auto', () => {
expect(responsiveTableColumn(1000, 'auto')).toBe('auto');
});
it('returns minWidth when minWidth is a number and greater than columnWidth', () => {
expect(responsiveTableColumn(1000, 80)).toBe(80);
});
it('returns ColumnSize.Small for width less than WidthThresholds.Small', () => {
expect(responsiveTableColumn(WidthThresholds.Small - 1, 50)).toBe(
ColumnSize.Default,
);
});
it('returns ColumnSize.Medium for width between WidthThresholds.Small and WidthThresholds.Medium', () => {
expect(responsiveTableColumn(WidthThresholds.Medium - 1, 50)).toBe(
ColumnSize.Small,
);
});
it('returns ColumnSize.Large for width between WidthThresholds.Medium and WidthThresholds.Large', () => {
expect(responsiveTableColumn(WidthThresholds.Large - 1, 50)).toBe(
ColumnSize.Medium,
);
});
it('returns ColumnSize.Large for width greater than or equal to WidthThresholds.Large', () => {
expect(responsiveTableColumn(WidthThresholds.Large, 50)).toBe(
ColumnSize.Large,
);
});
});

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.
*/
import { retryImport } from '../src/retry-import';
describe('retry-import tests', () => {
it('retryImport', async () => {
const maxRetryCount = 3;
let maxCount = 0;
const mockImport = () =>
new Promise<number>((resolve, reject) => {
setTimeout(() => {
if (maxCount >= maxRetryCount) {
resolve(maxCount);
return;
}
maxCount++;
reject(new Error('load error!'));
}, 1000);
});
expect(await retryImport<number>(() => mockImport(), maxRetryCount)).toBe(
3,
);
});
});

View File

@@ -0,0 +1,74 @@
/*
* 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 { expect, it } from 'vitest';
import { logger } from '@coze-arch/logger';
import { safeJSONParse, typeSafeJSONParse } from '../src/safe-json-parse';
vi.mock('@coze-arch/logger', () => ({
logger: {
persist: {
error: vi.fn(),
},
},
reporter: {
errorEvent: vi.fn(),
},
}));
describe('safe-json-parse', () => {
test('safeJSONParse without error', () => {
const test1 = '{}';
const res1 = safeJSONParse(test1);
expect(res1).toStrictEqual({});
const test2 = '[]';
const res2 = safeJSONParse(test2);
expect(res2).toStrictEqual([]);
expect(logger.persist.error).not.toHaveBeenCalled();
});
test('safeJSONParse with error', () => {
const test = '';
const res1 = safeJSONParse(test);
expect(res1).equal(undefined);
expect(logger.persist.error).toHaveBeenCalledTimes(1);
const expectValue = 'empty_value';
const res2 = safeJSONParse(test, expectValue);
expect(res2).equal(expectValue);
expect(logger.persist.error).toHaveBeenCalledTimes(2);
});
});
describe('type safe json parse', () => {
it('parse obj return as input', () => {
const ob = {};
expect(typeSafeJSONParse(ob)).toBe(ob);
});
it('parse legally', () => {
const ob = { a: 1 };
expect(typeSafeJSONParse(JSON.stringify(ob))).toMatchObject(ob);
});
it('throw error when illegal', () => {
const str = '{';
expect(typeSafeJSONParse(str)).toBeUndefined();
});
});

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.
*/
global.IS_OVERSEA = false;
global.APP_ID = '';
global.IMAGE_FALLBACK_HOST = '';
global.BYTE_UPLOADER_REGION = '';
global.SAMI_WS_ORIGIN = '';
global.SAMI_CHAT_WS_URL = '';
global.SAMI_APP_KEY = '';

View File

@@ -0,0 +1,34 @@
/*
* 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 { SkillKeyEnum } from '@coze-agent-ide/tool-config';
import { skillKeyToApiStatusKeyTransformer } from '../src/skill';
vi.stubGlobal('IS_DEV_MODE', false);
vi.mock('@coze-agent-ide/tool', () => ({
SkillKeyEnum: {
TEXT_TO_SPEECH: 'tts',
},
}));
describe('skill', () => {
test('skillKeyToApiStatusKeyTransformer', () => {
const test = SkillKeyEnum.TEXT_TO_SPEECH;
expect(skillKeyToApiStatusKeyTransformer(test)).equal(`${test}_tab_status`);
});
});

View File

@@ -0,0 +1,99 @@
/*
* 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 { uploadFileV2 } from '../src/upload-file-v2';
vi.mock('@coze-arch/bot-api', () => ({
DeveloperApi: {
GetUploadAuthToken: vi.fn(() =>
Promise.resolve({ data: { service_id: '', upload_host: '' } }),
),
},
}));
vi.mock('@coze-studio/uploader-adapter', () => {
class MockUploader {
start = () => 0;
addFile = () => '12312341';
test: 'test';
on(event: string, cb: (data: any) => void) {
if (event === 'complete') {
cb({ uploadResult: { Uri: 'test_url' } });
} else if (event === 'error') {
cb({ extra: 'error' });
} else if (event === 'progress') {
cb(50);
}
}
}
return {
getUploader: vi.fn((props: any, isOverSea?: boolean) => new MockUploader()),
};
});
describe('upload-file', () => {
afterEach(() => {
vi.clearAllMocks();
});
test('upLoadFile should resolve Url of result if upload success', () =>
new Promise((resolve, reject) => {
global.IS_OVERSEA = true;
uploadFileV2({
fileItemList: [{ file: new File([], 'test_file'), fileType: 'image' }],
userId: '123',
timeout: undefined,
signal: new AbortSignal(),
onSuccess: event => {
try {
expect(event.uploadResult.Uri).equal('test_url');
resolve('ok');
} catch (e) {
reject(e);
}
},
onUploadAllSuccess(event) {
try {
expect(event[0].uploadResult.Uri).equal('test_url');
resolve('ok');
} catch (e) {
reject(e);
}
},
});
global.IS_OVERSEA = false;
uploadFileV2({
fileItemList: [{ file: new File([], 'test_file'), fileType: 'image' }],
userId: '123',
timeout: undefined,
signal: new AbortSignal(),
onSuccess: event => {
try {
expect(event.uploadResult.Uri).equal('test_url');
resolve('ok');
} catch (e) {
reject(e);
}
},
onUploadAllSuccess(event) {
try {
expect(event[0].uploadResult.Uri).equal('test_url');
resolve('ok');
} catch (e) {
reject(e);
}
},
});
}));
});

View File

@@ -0,0 +1,110 @@
/*
* 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 Mock } from 'vitest';
import { userStoreService } from '@coze-studio/user-store';
import { upLoadFile } from '../src/upload-file';
vi.mock('@coze-arch/bot-api', () => ({
DeveloperApi: {
GetUploadAuthToken: vi.fn(() =>
Promise.resolve({ data: { service_id: '', upload_host: '' } }),
),
},
}));
vi.mock('@coze-studio/user-store', () => ({
userStoreService: {
getUserInfo: vi.fn(() => ({
user_id_str: '',
})),
},
}));
vi.mock('@coze-studio/uploader-adapter', () => {
class MockUploader {
userId: string;
constructor({ userId }) {
this.userId = userId;
}
on(event: string, cb: (data: any) => void) {
if (event === 'complete' && this.userId) {
cb({ uploadResult: { Uri: 'test_url' } });
} else if (event === 'error' && !this.userId) {
cb({ extra: 'error' });
} else if (event === 'progress') {
cb(50);
}
}
}
return {
getUploader: vi.fn(
(props: any, isOverSea?: boolean) => new MockUploader(props),
),
};
});
describe('upload-file', () => {
afterEach(() => {
vi.clearAllMocks();
});
test('upLoadFile should resolve Url of result if upload success', async () => {
// mock `userId` non-empty to invoke upload success
(userStoreService.getUserInfo as Mock).mockReturnValue({
user_id_str: 'test',
});
const res = await upLoadFile({
file: new File([], 'test_file'),
fileType: 'image',
});
expect(res).equal('test_url');
global.IS_OVERSEA = false;
(userStoreService.getUserInfo as Mock).mockReturnValue({
user_id_str: 'test',
});
const res2 = await upLoadFile({
file: new File([], 'test_file'),
fileType: 'image',
});
expect(res2).equal('test_url');
});
test('upLoadFile should reject extra info of result if upload failed', () => {
// mock `userId` empty to invoke upload failed
(userStoreService.getUserInfo as Mock).mockReturnValue({ user_id_str: '' });
expect(
upLoadFile({
file: new File([], 'test_file'),
fileType: 'image',
}),
).rejects.toThrow('error');
});
test('upLoadFile should use getUploadAuthToken if biz is not bot or workflow ', () => {
// mock `userId` empty to invoke upload failed
(userStoreService.getUserInfo as Mock).mockReturnValue({ user_id_str: '' });
expect(
upLoadFile({
biz: 'community',
file: new File([], 'test_file'),
fileType: 'image',
getUploadAuthToken: vi.fn(() =>
Promise.resolve({ data: { service_id: '', upload_host: '' } }),
),
}),
).rejects.toThrow('error');
});
});

View File

@@ -0,0 +1,100 @@
/*
* 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 { appendUrlParam, getParamsFromQuery, openUrl } from '../src/url';
import { getIsMobile, getIsSafari } from '../src/platform';
vi.mock('../src/platform', () => ({
getIsMobile: vi.fn(),
getIsSafari: vi.fn(),
}));
const mockParseQuery = (queryStr: string) =>
Object.fromEntries(
queryStr
.split('&')
.map(str => {
const parts = str.split('=');
return [parts[0] ?? '', parts?.[1] ?? ''];
})
.filter(entries => !!entries[0]),
);
vi.mock('query-string', () => ({
default: {
parseUrl: (url: string) => {
const [rawUrl, queryStr = ''] = url.split('?');
return {
url: rawUrl,
query: mockParseQuery(queryStr),
};
},
parse: (queryStr: string) => mockParseQuery(queryStr.slice(1)),
stringifyUrl: ({ url, query }: { url: string; query: string }) =>
`${url}${Object.entries(query).length ? '?' : ''}${Object.entries(query)
.map(entry => entry.join('='))
.join('&')}`,
},
}));
describe('URL', () => {
beforeEach(() => {
vi.stubGlobal('location', { href: '' });
vi.stubGlobal('window', {
open: vi.fn(),
});
});
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
});
it('#getParamsFromQuery', () => {
vi.stubGlobal('location', { search: '' });
expect(getParamsFromQuery({ key: '' })).toEqual('');
expect(getParamsFromQuery({ key: 'a' })).toEqual('');
vi.stubGlobal('location', { search: '?a=b' });
expect(getParamsFromQuery({ key: '' })).toEqual('');
expect(getParamsFromQuery({ key: 'a' })).toEqual('b');
});
it('#appendUrlParam', () => {
expect(appendUrlParam('http://test.com', 'k1', 'v1')).equal(
'http://test.com?k1=v1',
);
expect(appendUrlParam('http://test.com?k1=v1', 'k2', 'v2')).equal(
'http://test.com?k1=v1&k2=v2',
);
expect(appendUrlParam('http://test.com?k1=v1', 'k1', '')).equal(
'http://test.com',
);
});
it('#openUrl', () => {
openUrl(undefined);
expect(window.open).not.toHaveBeenCalled();
expect(location.href).toBe('');
vi.mocked(getIsMobile).mockReturnValue(true);
vi.mocked(getIsSafari).mockReturnValue(true);
openUrl('https://example.com');
expect(location.href).toBe('https://example.com');
expect(window.open).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,39 @@
/*
* 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 { setMobileBody, setPCBody } from '../src/viewport';
describe('viewport', () => {
it('#setMobileBody', () => {
setMobileBody();
const bodyStyle = document?.body?.style;
const htmlStyle = document?.getElementsByTagName('html')?.[0]?.style;
expect(bodyStyle.minWidth).toEqual('0');
expect(bodyStyle.minHeight).toEqual('0');
expect(htmlStyle.minWidth).toEqual('0');
expect(htmlStyle.minHeight).toEqual('0');
});
it('#setPCBody', () => {
setPCBody();
const bodyStyle = document?.body?.style;
const htmlStyle = document?.getElementsByTagName('html')?.[0]?.style;
expect(bodyStyle.minWidth).toEqual('1200px');
expect(bodyStyle.minHeight).toEqual('600px');
expect(htmlStyle.minWidth).toEqual('1200px');
expect(htmlStyle.minHeight).toEqual('600px');
});
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View File

@@ -0,0 +1,78 @@
{
"name": "@coze-arch/bot-utils",
"version": "0.0.1",
"description": "common utils extracts from apps/bot",
"license": "Apache-2.0",
"author": "fanwenjie.fe@bytedance.com",
"maintainers": [],
"exports": {
".": "./src/index.ts",
"./voice-assistant": "./src/voice-assistant.ts",
"./upload-file-v2": "./src/upload-file-v2.ts",
"./post-message-channel": "./src/post-message-channel.ts",
"./date": "./src/date.ts"
},
"main": "src/index.ts",
"typesVersions": {
"*": {
".": [
"./src/index.ts"
],
"upload-file-v2": [
"./src/upload-file-v2.ts"
],
"post-message-channel": [
"./src/post-message-channel.ts"
],
"date": [
"./src/date.ts"
]
}
},
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-agent-ide/tool-config": "workspace:*",
"@coze-arch/bot-error": "workspace:*",
"@coze-studio/uploader-adapter": "workspace:*",
"@coze-studio/user-store": "workspace:*",
"@coze-arch/bot-api": "workspace:*",
"@coze-arch/bot-flags": "workspace:*",
"@coze-arch/bot-studio-store": "workspace:*",
"@coze-arch/i18n": "workspace:*",
"@coze-arch/logger": "workspace:*",
"@coze-arch/report-events": "workspace:*",
"@coze-arch/tea": "workspace:*",
"@coze-arch/web-context": "workspace:*",
"bowser": "2.11.0",
"dayjs": "^1.11.7",
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
"md5": "^2.3.0",
"query-string": "^8.1.0"
},
"devDependencies": {
"@coze-common/chat-core": "workspace:*",
"@coze-arch/bot-typings": "workspace:*",
"@coze-arch/eslint-config": "workspace:*",
"@coze-arch/ts-config": "workspace:*",
"@coze-arch/vitest-config": "workspace:*",
"@types/lodash-es": "^4.17.10",
"@types/md5": "^2.3.2",
"@types/node": "^18",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@vitest/coverage-v8": "~3.0.5",
"sucrase": "^3.32.0",
"vitest": "~3.0.5"
},
"peerDependencies": {
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
}

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';
}
};

View File

@@ -0,0 +1,67 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"types": ["lodash-es"],
"strictNullChecks": true,
"rootDir": "./src",
"skipLibCheck": true,
"disableReferencedProjectLoad": true,
"outDir": "./dist",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../agent-ide/tool-config/tsconfig.build.json"
},
{
"path": "../bot-api/tsconfig.build.json"
},
{
"path": "../bot-error/tsconfig.build.json"
},
{
"path": "../bot-flags/tsconfig.build.json"
},
{
"path": "../bot-store/tsconfig.build.json"
},
{
"path": "../bot-typings/tsconfig.build.json"
},
{
"path": "../../common/chat-area/chat-core/tsconfig.build.json"
},
{
"path": "../../common/uploader-adapter/tsconfig.build.json"
},
{
"path": "../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../config/vitest-config/tsconfig.build.json"
},
{
"path": "../i18n/tsconfig.build.json"
},
{
"path": "../logger/tsconfig.build.json"
},
{
"path": "../report-events/tsconfig.build.json"
},
{
"path": "../../studio/user-store/tsconfig.build.json"
},
{
"path": "../tea/tsconfig.build.json"
},
{
"path": "../web-context/tsconfig.build.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View File

@@ -0,0 +1,17 @@
{
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["__tests__", "vitest.config.ts"],
"exclude": ["./dist"],
"references": [
{
"path": "./tsconfig.build.json"
}
],
"compilerOptions": {
"rootDir": "./",
"outDir": "./dist",
"types": ["vitest/globals"],
"strictNullChecks": true
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
dirname: __dirname,
preset: 'web',
test: {
setupFiles: ['./__tests__/setup.ts'],
},
plugins: [
{
name: 'edenx-virtual-modules',
enforce: 'pre',
},
],
});