feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
77
frontend/packages/arch/bot-utils/README.md
Normal file
77
frontend/packages/arch/bot-utils/README.md
Normal 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
|
||||
70
frontend/packages/arch/bot-utils/__tests__/array.test.ts
Normal file
70
frontend/packages/arch/bot-utils/__tests__/array.test.ts
Normal 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' }]);
|
||||
});
|
||||
});
|
||||
114
frontend/packages/arch/bot-utils/__tests__/date.test.ts
Normal file
114
frontend/packages/arch/bot-utils/__tests__/date.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
45
frontend/packages/arch/bot-utils/__tests__/dom.test.ts
Normal file
45
frontend/packages/arch/bot-utils/__tests__/dom.test.ts
Normal 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();
|
||||
});
|
||||
@@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
30
frontend/packages/arch/bot-utils/__tests__/html.test.tsx
Normal file
30
frontend/packages/arch/bot-utils/__tests__/html.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
||||
47
frontend/packages/arch/bot-utils/__tests__/image.test.ts
Normal file
47
frontend/packages/arch/bot-utils/__tests__/image.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
21
frontend/packages/arch/bot-utils/__tests__/index.test.ts
Normal file
21
frontend/packages/arch/bot-utils/__tests__/index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
38
frontend/packages/arch/bot-utils/__tests__/is-mobile.test.ts
Normal file
38
frontend/packages/arch/bot-utils/__tests__/is-mobile.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
123
frontend/packages/arch/bot-utils/__tests__/number.test.ts
Normal file
123
frontend/packages/arch/bot-utils/__tests__/number.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
23
frontend/packages/arch/bot-utils/__tests__/setup.ts
Normal file
23
frontend/packages/arch/bot-utils/__tests__/setup.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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 = '';
|
||||
34
frontend/packages/arch/bot-utils/__tests__/skill.test.ts
Normal file
34
frontend/packages/arch/bot-utils/__tests__/skill.test.ts
Normal 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`);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
110
frontend/packages/arch/bot-utils/__tests__/upload-file.test.ts
Normal file
110
frontend/packages/arch/bot-utils/__tests__/upload-file.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
100
frontend/packages/arch/bot-utils/__tests__/url.test.ts
Normal file
100
frontend/packages/arch/bot-utils/__tests__/url.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
39
frontend/packages/arch/bot-utils/__tests__/viewport.test.ts
Normal file
39
frontend/packages/arch/bot-utils/__tests__/viewport.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
12
frontend/packages/arch/bot-utils/config/rush-project.json
Normal file
12
frontend/packages/arch/bot-utils/config/rush-project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
7
frontend/packages/arch/bot-utils/eslint.config.js
Normal file
7
frontend/packages/arch/bot-utils/eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
78
frontend/packages/arch/bot-utils/package.json
Normal file
78
frontend/packages/arch/bot-utils/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { typeSafeJSONParse } from './safe-json-parse';
|
||||
|
||||
export function arrayBufferToObject(
|
||||
buffer: ArrayBuffer,
|
||||
encoding = 'utf-8',
|
||||
): Record<string, unknown> {
|
||||
try {
|
||||
const decoder = new TextDecoder(encoding);
|
||||
const string = decoder.decode(buffer);
|
||||
return typeSafeJSONParse(string) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
120
frontend/packages/arch/bot-utils/src/array.ts
Normal file
120
frontend/packages/arch/bot-utils/src/array.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isFunction } from 'lodash-es';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Obj = Record<string, any>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const ArrayUtil = {
|
||||
array2Map,
|
||||
mapAndFilter,
|
||||
};
|
||||
|
||||
// region array2Map 重载声明
|
||||
// 和 OptionUtil.array2Map 虽然相似,但在用法和类型约束上还是很不一样的
|
||||
/**
|
||||
* 将列表转化为 map
|
||||
* @param items
|
||||
* @param key 指定 item[key] 作为 map 的键
|
||||
* @example
|
||||
* const items = [{name: 'a', id: 1}];
|
||||
* array2Map(items, 'id');
|
||||
* // {1: {name: 'a', id: 1}}
|
||||
*/
|
||||
function array2Map<T extends Obj, K extends keyof T>(
|
||||
items: T[],
|
||||
key: K,
|
||||
): Record<T[K], T>;
|
||||
/**
|
||||
* 将列表转化为 map
|
||||
* @param items
|
||||
* @param key 指定 item[key] 作为 map 的键
|
||||
* @param value 指定 item[value] 作为 map 的值
|
||||
* @example
|
||||
* const items = [{name: 'a', id: 1}];
|
||||
* array2Map(items, 'id', 'name');
|
||||
* // {1: 'a'}
|
||||
*/
|
||||
function array2Map<T extends Obj, K extends keyof T, V extends keyof T>(
|
||||
items: T[],
|
||||
key: K,
|
||||
value: V,
|
||||
): Record<T[K], T[V]>;
|
||||
/**
|
||||
* 将列表转化为 map
|
||||
* @param items
|
||||
* @param key 指定 item[key] 作为 map 的键
|
||||
* @param value 获取值
|
||||
* @example
|
||||
* const items = [{name: 'a', id: 1}];
|
||||
* array2Map(items, 'id', (item) => `${item.id}-${item.name}`);
|
||||
* // {1: '1-a'}
|
||||
*/
|
||||
function array2Map<T extends Obj, K extends keyof T, V>(
|
||||
items: T[],
|
||||
key: K,
|
||||
value: (item: T) => V,
|
||||
): Record<T[K], V>;
|
||||
// endregion
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/** 将列表转化为 map */
|
||||
function array2Map<T extends Obj, K extends keyof T>(
|
||||
items: T[],
|
||||
key: K,
|
||||
value: keyof T | ((item: T) => any) = item => item,
|
||||
): Partial<Record<T[K], any>> {
|
||||
return items.reduce((map, item) => {
|
||||
const currKey = String(item[key]);
|
||||
const currValue = isFunction(value) ? value(item) : item[value];
|
||||
return { ...map, [currKey]: currValue };
|
||||
}, {});
|
||||
}
|
||||
|
||||
function mapAndFilter<I extends Obj = Obj>(
|
||||
target: Array<I>,
|
||||
options?: {
|
||||
filter?: (item: I) => boolean;
|
||||
},
|
||||
): Array<I>;
|
||||
function mapAndFilter<I extends Obj = Obj, T extends Obj = Obj>(
|
||||
target: Array<I>,
|
||||
options: {
|
||||
filter?: (item: I) => boolean;
|
||||
map: (item: I) => T;
|
||||
},
|
||||
): Array<T>;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
function mapAndFilter<I = Obj, T = Obj>(
|
||||
target: Array<I>,
|
||||
options: {
|
||||
filter?: (item: I) => boolean;
|
||||
map?: (item: I) => T;
|
||||
} = {},
|
||||
) {
|
||||
const { filter, map } = options;
|
||||
return target.reduce((previousValue, currentValue) => {
|
||||
const realValue = map ? map(currentValue) : currentValue;
|
||||
const filtered = filter ? filter(currentValue) : true;
|
||||
if (!filtered) {
|
||||
// 如果filtered是false,表示此项需要跳过
|
||||
return previousValue;
|
||||
}
|
||||
// 如果filtered是true,表示需要加上此项
|
||||
return [...previousValue, realValue] as Array<I>;
|
||||
}, [] as Array<I>);
|
||||
}
|
||||
66
frontend/packages/arch/bot-utils/src/cache.ts
Normal file
66
frontend/packages/arch/bot-utils/src/cache.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
type Timer = ReturnType<typeof setTimeout>;
|
||||
type CachedKey = string | number;
|
||||
|
||||
export interface CachedData<TData = unknown> {
|
||||
data: TData;
|
||||
time: number;
|
||||
}
|
||||
export interface RecordData extends CachedData {
|
||||
timer: Timer | undefined;
|
||||
}
|
||||
|
||||
const cache = new Map<CachedKey, RecordData>();
|
||||
|
||||
const setCache = (
|
||||
key: CachedKey,
|
||||
cacheTime: number,
|
||||
cachedData: CachedData,
|
||||
) => {
|
||||
const currentCache = cache.get(key);
|
||||
if (currentCache?.timer) {
|
||||
clearTimeout(currentCache.timer);
|
||||
}
|
||||
|
||||
let timer: Timer | undefined = undefined;
|
||||
|
||||
if (cacheTime > -1) {
|
||||
// if cache out, clear it
|
||||
timer = setTimeout(() => {
|
||||
cache.delete(key);
|
||||
}, cacheTime);
|
||||
}
|
||||
|
||||
cache.set(key, {
|
||||
...cachedData,
|
||||
timer,
|
||||
});
|
||||
};
|
||||
|
||||
const getCache = (key: CachedKey) => cache.get(key);
|
||||
|
||||
const clearCache = (key?: string | string[]) => {
|
||||
if (key) {
|
||||
const cacheKeys = Array.isArray(key) ? key : [key];
|
||||
cacheKeys.forEach(cacheKey => cache.delete(cacheKey));
|
||||
} else {
|
||||
cache.clear();
|
||||
}
|
||||
};
|
||||
|
||||
export { getCache, setCache, clearCache };
|
||||
129
frontend/packages/arch/bot-utils/src/date.ts
Normal file
129
frontend/packages/arch/bot-utils/src/date.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import dayjsUTC from 'dayjs/plugin/utc';
|
||||
import dayjsTimezone from 'dayjs/plugin/timezone';
|
||||
import dayjsDuration from 'dayjs/plugin/duration';
|
||||
import dayjs, { type ManipulateType, type ConfigType, type Dayjs } from 'dayjs';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
dayjs.extend(dayjsUTC);
|
||||
dayjs.extend(dayjsTimezone);
|
||||
dayjs.extend(dayjsDuration);
|
||||
|
||||
const FORMAT_DATE_MAP = {
|
||||
Today: 'HH:mm',
|
||||
CurrentYear: 'MM-DD HH:mm',
|
||||
Default: 'YYYY-MM-DD HH:mm',
|
||||
};
|
||||
|
||||
export const getFormatDateType = (time: number) => {
|
||||
const compareTime = dayjs.unix(time);
|
||||
const currentTime = dayjs();
|
||||
if (compareTime.isSame(currentTime, 'day')) {
|
||||
return FORMAT_DATE_MAP.Today;
|
||||
}
|
||||
if (compareTime.isSame(currentTime, 'year')) {
|
||||
return FORMAT_DATE_MAP.CurrentYear;
|
||||
}
|
||||
return FORMAT_DATE_MAP.Default;
|
||||
};
|
||||
|
||||
export const formatDate = (v: number, template = 'YYYY/MM/DD HH:mm:ss') =>
|
||||
dayjs.unix(v).format(template);
|
||||
|
||||
export const CHINESE_TIMEZONE = 'Asia/Shanghai';
|
||||
|
||||
// 根据地区判断 海外返回UTC时间,国内返回北京时间
|
||||
export const getCurrentTZ = (param?: ConfigType): Dayjs => {
|
||||
if (IS_OVERSEA) {
|
||||
return dayjs(param).utc(true);
|
||||
}
|
||||
return dayjs(param).tz(CHINESE_TIMEZONE, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取dayjs add后的时间戳
|
||||
*/
|
||||
export const getTimestampByAdd = (value: number, unit?: ManipulateType) =>
|
||||
dayjs().add(value, unit).unix();
|
||||
|
||||
/**
|
||||
* 获取当前的时间戳
|
||||
*/
|
||||
export const getCurrentTimestamp = () => dayjs().unix();
|
||||
|
||||
/**
|
||||
* 获取当前时间到次日UTC0点的时间间隔,精确到分钟
|
||||
* e.g. 12h 30m
|
||||
*/
|
||||
export const getRemainTime = () => {
|
||||
const now = dayjs.utc();
|
||||
const nextDay = now.add(1, 'day').startOf('day');
|
||||
const diff = nextDay.diff(now);
|
||||
const duration = dayjs.duration(diff);
|
||||
const hour = duration.hours();
|
||||
const minute = duration.minutes();
|
||||
return `${hour}h ${minute}m`;
|
||||
};
|
||||
|
||||
/**
|
||||
* fork 自 packages/community/pages/src/bot/utils/index.ts
|
||||
* 将11位的时间戳按以下格式显示
|
||||
* 1. 不足一分钟, 显示”Just now“
|
||||
* 2. 不足1小时, 显示”{n}min ago“,例如 3min ago
|
||||
* 3. 不足1天,显示”{n}h ago",例如 3h ago
|
||||
* 4. 不足1个月,显示"{n}d ago", 例如 3d ago
|
||||
* 5. 超过1个月,显示“{MM}/{DD}/{yyyy}”,例如12/1/2024,中文是2024 年 12 月 1 日
|
||||
*
|
||||
*/
|
||||
export const formatTimestamp = (timestampMs: number) => {
|
||||
/** 秒级时间戳 */
|
||||
const timestampSecond = Math.floor(timestampMs / 1000);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const diff = now - timestampSecond;
|
||||
|
||||
// 将时间差转换成分钟、小时和天数
|
||||
const minutes = Math.floor(diff / 60);
|
||||
const hours = Math.floor(diff / 3600);
|
||||
const days = Math.floor(diff / 86400);
|
||||
|
||||
// 不足一分钟,显示“Just now”
|
||||
if (minutes < 1) {
|
||||
return I18n.t('community_time_just_now');
|
||||
}
|
||||
// 不足一小时,显示“{n}min ago”
|
||||
else if (hours < 1) {
|
||||
return I18n.t('community_time_min', { n: minutes });
|
||||
}
|
||||
// 不足一天,显示“{n}h ago”
|
||||
else if (days < 1) {
|
||||
return I18n.t('community_time_hour', { n: hours });
|
||||
}
|
||||
// 不足一个月,显示“{n}d ago”
|
||||
else if (days < 30) {
|
||||
return I18n.t('community_time_day', { n: days });
|
||||
}
|
||||
// 超过一个月,显示“{MM}/{DD}/{yyyy}”
|
||||
else {
|
||||
const dayObj = dayjs(timestampSecond * 1000);
|
||||
return I18n.t('community_time_date', {
|
||||
yyyy: dayObj.get('y'),
|
||||
mm: dayObj.get('M') + 1,
|
||||
dd: dayObj.get('D'),
|
||||
});
|
||||
}
|
||||
};
|
||||
68
frontend/packages/arch/bot-utils/src/dom.ts
Normal file
68
frontend/packages/arch/bot-utils/src/dom.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取最近一个可滚动元素
|
||||
*/
|
||||
export function closestScrollableElement(element: HTMLElement) {
|
||||
const htmlElement = document.documentElement;
|
||||
if (!element) {
|
||||
return htmlElement;
|
||||
}
|
||||
let style = window.getComputedStyle(element);
|
||||
const excludeStaticParent = style.position === 'absolute';
|
||||
const overflowReg = /(auto|scroll|overlay)/;
|
||||
|
||||
if (style.position === 'fixed') {
|
||||
return htmlElement;
|
||||
}
|
||||
let parent = element;
|
||||
while (parent) {
|
||||
style = window.getComputedStyle(parent);
|
||||
if (excludeStaticParent && style.position === 'static') {
|
||||
parent = parent.parentElement as HTMLElement;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
overflowReg.test(style.overflow + style.overflowY + style.overflowX) ||
|
||||
parent.getAttribute('data-overflow') === 'true'
|
||||
) {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parentElement as HTMLElement;
|
||||
}
|
||||
return htmlElement;
|
||||
}
|
||||
|
||||
// 解决浏览器拦截window.open行为,接口catch则跳错误兜底页
|
||||
export const openNewWindow = async (
|
||||
callbackUrl: () => Promise<string> | string,
|
||||
defaultUrl?: string,
|
||||
) => {
|
||||
const newWindow = window.open(defaultUrl || '');
|
||||
|
||||
let url = '';
|
||||
try {
|
||||
url = await callbackUrl();
|
||||
} catch (error) {
|
||||
url = `${location.origin}/404`;
|
||||
newWindow?.close();
|
||||
}
|
||||
|
||||
if (newWindow) {
|
||||
newWindow.location = url;
|
||||
}
|
||||
};
|
||||
159
frontend/packages/arch/bot-utils/src/event-handler.ts
Normal file
159
frontend/packages/arch/bot-utils/src/event-handler.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { type AbilityKey } from '@coze-agent-ide/tool-config';
|
||||
|
||||
interface EventWithData<T extends EventEmitter.ValidEventTypes> {
|
||||
event: EventEmitter.EventNames<T>;
|
||||
args: Parameters<EventEmitter.EventListener<T, EventEmitter.EventNames<T>>>;
|
||||
}
|
||||
|
||||
export class BufferedEventEmitter<T extends EventEmitter.ValidEventTypes> {
|
||||
eventEmitter = new EventEmitter<T>();
|
||||
|
||||
started = true;
|
||||
|
||||
buffer: EventWithData<T>[] = [];
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
* @param event 事件名称
|
||||
* @param args 参数
|
||||
*/
|
||||
emit<P extends EventEmitter.EventNames<T>>(
|
||||
event: P,
|
||||
...args: Parameters<EventEmitter.EventListener<T, P>>
|
||||
) {
|
||||
if (!this.started) {
|
||||
this.buffer.push({
|
||||
event,
|
||||
args,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.eventEmitter.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅事件
|
||||
* @param event 事件名称
|
||||
* @param fn 事件回调
|
||||
*/
|
||||
on<P extends EventEmitter.EventNames<T>>(
|
||||
event: P,
|
||||
fn: EventEmitter.EventListener<T, P>,
|
||||
) {
|
||||
this.eventEmitter.on(event, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅事件
|
||||
* @param event 事件名称
|
||||
* @param fn 事件回调
|
||||
*/
|
||||
off<P extends EventEmitter.EventNames<T>>(
|
||||
event: P,
|
||||
fn: EventEmitter.EventListener<T, P>,
|
||||
) {
|
||||
this.eventEmitter.off(event, fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启缓存事件订阅器,开启时会将关闭时收到的事件对应的回调按顺序逐一触发
|
||||
*/
|
||||
start() {
|
||||
this.started = true;
|
||||
for (const { event, args } of this.buffer) {
|
||||
this.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭缓存事件订阅器,在关闭时收到的事件会被缓存并延迟到下次开启时触发
|
||||
*/
|
||||
stop() {
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存事件订阅器缓存的事件,使得在重新开启(start)时不会触发在关闭(stop)时收到的事件对应的回调
|
||||
*/
|
||||
clear() {
|
||||
this.buffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
let eventEmitter: BufferedEventEmitter<EmitEventType> | null = null;
|
||||
|
||||
const initEventEmitter = () => {
|
||||
if (!eventEmitter) {
|
||||
eventEmitter = new BufferedEventEmitter<EmitEventType>();
|
||||
}
|
||||
};
|
||||
|
||||
// 模块折叠 有关事件
|
||||
export enum OpenBlockEvent {
|
||||
DATA_MEMORY_BLOCK_OPEN = 'dataMemoryBlockOpen',
|
||||
TABLE_MEMORY_BLOCK_OPEN = 'tableMemoryBlockOpen',
|
||||
DATA_SET_BLOCK_OPEN = 'dataSetBlockOpen',
|
||||
TIME_CAPSULE_BLOCK_OPEN = 'timeCapsuleBlockOpen',
|
||||
ONBORDING_MESSAGE_BLOCK_OPEN = 'onbordingMessageBlockOpen',
|
||||
PLUGIN_API_BLOCK_OPEN = 'pluginApiBlockOpen',
|
||||
WORKFLOW_BLOCK_OPEN = 'workflowBlockOpen',
|
||||
IMAGEFLOW_BLOCK_OPEN = 'imageBlockOpen',
|
||||
TASK_MANAGE_OPEN = 'taskManageOpen',
|
||||
SUGGESTION_BLOCK_OPEN = 'suggestionBlockOpen',
|
||||
TTS_BLOCK_OPEN = 'TTSBlockOpen',
|
||||
FILEBOX_OPEN = 'FileboxOpen',
|
||||
BACKGROUND_IMAGE_BLOCK = 'BackgroundImageOpen',
|
||||
}
|
||||
|
||||
// 模块弹窗 有关事件
|
||||
export enum OpenModalEvent {
|
||||
PLUGIN_API_MODAL_OPEN = 'pluginApiModalOpen',
|
||||
}
|
||||
|
||||
export type EmitEventType = OpenBlockEvent | OpenModalEvent | AbilityKey;
|
||||
export const emitEvent = (event: EmitEventType, ...data: unknown[]) => {
|
||||
initEventEmitter();
|
||||
|
||||
eventEmitter?.emit(event, ...data);
|
||||
};
|
||||
|
||||
export const handleEvent = (
|
||||
event: EmitEventType,
|
||||
fn: (...args: unknown[]) => void,
|
||||
) => {
|
||||
initEventEmitter();
|
||||
|
||||
eventEmitter?.on(event, fn);
|
||||
};
|
||||
|
||||
export const removeEvent = (
|
||||
event: EmitEventType,
|
||||
fn: (...args: unknown[]) => void,
|
||||
) => {
|
||||
initEventEmitter();
|
||||
|
||||
eventEmitter?.off(event, fn);
|
||||
};
|
||||
|
||||
export enum DraftEvent {
|
||||
DELETE_VARIABLE = 'deleteVariable',
|
||||
}
|
||||
|
||||
export const draftEventEmitter = new EventEmitter();
|
||||
56
frontend/packages/arch/bot-utils/src/get-report-error.ts
Normal file
56
frontend/packages/arch/bot-utils/src/get-report-error.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isObject } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* @param inputError 传啥都行,一般是 catch (e) 那个 e
|
||||
* @param reason 解释
|
||||
*/
|
||||
export const getReportError = (
|
||||
inputError: unknown,
|
||||
reason?: string,
|
||||
): {
|
||||
error: Error;
|
||||
meta: Record<string, unknown>;
|
||||
} => {
|
||||
if (inputError instanceof Error) {
|
||||
return {
|
||||
error: inputError,
|
||||
meta: { reason },
|
||||
};
|
||||
}
|
||||
if (!isObject(inputError)) {
|
||||
return {
|
||||
error: new Error(String(inputError)),
|
||||
meta: { reason },
|
||||
};
|
||||
}
|
||||
return {
|
||||
error: new Error(''),
|
||||
meta: { ...covertInputObject(inputError), reason },
|
||||
};
|
||||
};
|
||||
|
||||
const covertInputObject = (inputError: object) => {
|
||||
if ('reason' in inputError) {
|
||||
return {
|
||||
...inputError,
|
||||
reasonOfInputError: inputError.reason,
|
||||
};
|
||||
}
|
||||
return inputError;
|
||||
};
|
||||
17
frontend/packages/arch/bot-utils/src/global.d.ts
vendored
Normal file
17
frontend/packages/arch/bot-utils/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
28
frontend/packages/arch/bot-utils/src/html.ts
Normal file
28
frontend/packages/arch/bot-utils/src/html.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { isString } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
export const renderHtmlTitle = (prefix?: ReactNode) => {
|
||||
const platformName = I18n.t('platform_name');
|
||||
if (isString(prefix)) {
|
||||
return `${prefix} - ${platformName}`;
|
||||
}
|
||||
return platformName;
|
||||
};
|
||||
23
frontend/packages/arch/bot-utils/src/image.ts
Normal file
23
frontend/packages/arch/bot-utils/src/image.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const loadImage = (url: string): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve();
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
});
|
||||
84
frontend/packages/arch/bot-utils/src/index.ts
Normal file
84
frontend/packages/arch/bot-utils/src/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { arrayBufferToObject } from './array-buffer-to-object';
|
||||
|
||||
export { isMobile } from './is-mobile';
|
||||
export { safeJSONParse, typeSafeJSONParse } from './safe-json-parse';
|
||||
export { type BytedUploader, upLoadFile } from './upload-file';
|
||||
export { messageReportEvent, type MessageReportEvent } from './message-report';
|
||||
export { ArrayUtil } from './array';
|
||||
export { skillKeyToApiStatusKeyTransformer } from './skill';
|
||||
export { loadImage } from './image';
|
||||
export { renderHtmlTitle } from './html';
|
||||
export { getParamsFromQuery, appendUrlParam, openUrl } from './url';
|
||||
export { responsiveTableColumn } from './responsive-table-column';
|
||||
export {
|
||||
getFormatDateType,
|
||||
formatDate,
|
||||
getCurrentTZ,
|
||||
getTimestampByAdd,
|
||||
getCurrentTimestamp,
|
||||
formatTimestamp,
|
||||
} from './date';
|
||||
export {
|
||||
simpleformatNumber,
|
||||
formatBytes,
|
||||
formatNumber,
|
||||
formatPercent,
|
||||
formatTime,
|
||||
getEllipsisCount,
|
||||
exhaustiveCheck,
|
||||
sleep,
|
||||
} from './number';
|
||||
|
||||
export {
|
||||
uploadFileV2,
|
||||
type EventPayloadMaps,
|
||||
type UploaderInstance,
|
||||
type UploadFileV2Param,
|
||||
type FileItem,
|
||||
} from './upload-file-v2';
|
||||
export { retryImport } from './retry-import';
|
||||
|
||||
export {
|
||||
BufferedEventEmitter,
|
||||
OpenBlockEvent,
|
||||
OpenModalEvent,
|
||||
EmitEventType,
|
||||
emitEvent,
|
||||
handleEvent,
|
||||
removeEvent,
|
||||
DraftEvent,
|
||||
draftEventEmitter,
|
||||
} from './event-handler';
|
||||
export { setMobileBody, setPCBody } from './viewport';
|
||||
/** 获取设备信息 */
|
||||
export {
|
||||
getIsIPhoneOrIPad,
|
||||
getIsIPad,
|
||||
getIsMobile,
|
||||
getIsMobileOrIPad,
|
||||
getIsSafari,
|
||||
} from './platform';
|
||||
export { closestScrollableElement, openNewWindow } from './dom';
|
||||
|
||||
export const timeoutPromise = (ms: number): Promise<void> =>
|
||||
new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
export { getCache, setCache, clearCache } from './cache';
|
||||
22
frontend/packages/arch/bot-utils/src/is-mobile.ts
Normal file
22
frontend/packages/arch/bot-utils/src/is-mobile.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const MIN_SCREEN_WIDTH = 640;
|
||||
|
||||
export const isMobile = (): boolean => {
|
||||
const width = document.documentElement.clientWidth;
|
||||
return width <= MIN_SCREEN_WIDTH;
|
||||
};
|
||||
287
frontend/packages/arch/bot-utils/src/message-report.ts
Normal file
287
frontend/packages/arch/bot-utils/src/message-report.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { isObject } from 'lodash-es';
|
||||
import { type ContentType, type Message } from '@coze-common/chat-core';
|
||||
import { globalVars } from '@coze-arch/web-context';
|
||||
import {
|
||||
type ReportEvent,
|
||||
REPORT_EVENTS as ReportEventNames,
|
||||
createReportEvent,
|
||||
} from '@coze-arch/report-events';
|
||||
import { reporter } from '@coze-arch/logger';
|
||||
import { CustomError } from '@coze-arch/bot-error';
|
||||
|
||||
// 这段代码是从 apps/bot/src/store/socket/utils.ts 复制出来的,后续也可以考虑统一
|
||||
const hasSuggestion = (ext?: unknown) =>
|
||||
isObject(ext) && 'has_suggest' in ext && ext.has_suggest === '1';
|
||||
|
||||
interface ErrorPayload {
|
||||
reason: string;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
const overtime = 120000;
|
||||
|
||||
export class MessageReportEvent {
|
||||
botID = '';
|
||||
|
||||
private _timer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _receivingMessages = false;
|
||||
private _receivingSuggests = false;
|
||||
private _hasReceiveFirstChunk = false;
|
||||
private _hasReceiveFirstSuggestChunk = false;
|
||||
private _messageTotalContent = 0;
|
||||
|
||||
private _executeDraftBotEvent?: ReportEvent;
|
||||
private _receiveMessagesEvent?: ReportEvent;
|
||||
private _messageReceiveSuggestsEvent?: ReportEvent;
|
||||
private _receiveTotalMessagesReportEvent?: ReportEvent;
|
||||
|
||||
getLogID() {
|
||||
const logId = globalVars.LAST_EXECUTE_ID;
|
||||
return { log_id: logId };
|
||||
}
|
||||
|
||||
getMetaCtx() {
|
||||
return {
|
||||
bot_id: this.botID,
|
||||
...this.getLogID(),
|
||||
};
|
||||
}
|
||||
|
||||
private _createExecuteDraftBotEvent = () =>
|
||||
createReportEvent({
|
||||
eventName: ReportEventNames.botDebugMessageSubmit,
|
||||
meta: this.getMetaCtx(),
|
||||
});
|
||||
private _createReceiveMessagesEvent = () =>
|
||||
createReportEvent({
|
||||
eventName: ReportEventNames.receiveMessage,
|
||||
meta: this.getMetaCtx(),
|
||||
});
|
||||
private _createMessageReceiveSuggestsEvent = () =>
|
||||
createReportEvent({
|
||||
eventName: ReportEventNames.messageReceiveSuggests,
|
||||
meta: this.getMetaCtx(),
|
||||
});
|
||||
private _createReceiveTotalMessagesEvent = () =>
|
||||
createReportEvent({
|
||||
eventName: ReportEventNames.receiveTotalMessages,
|
||||
meta: this.getMetaCtx(),
|
||||
});
|
||||
|
||||
private _receiveMessagesEventGate = () => this._receivingMessages;
|
||||
private _messageReceiveSuggestsEventGate = () => this._receivingSuggests;
|
||||
|
||||
private _clearTimeout() {
|
||||
if (!this._timer) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(this._timer);
|
||||
this._timer = void 0;
|
||||
}
|
||||
|
||||
interrupt() {
|
||||
this._clearTimeout();
|
||||
|
||||
if (this._receivingMessages || this._receivingSuggests) {
|
||||
this._receiveTotalMessagesEvent.success();
|
||||
if (this._receivingMessages) {
|
||||
this.receiveMessageEvent.success();
|
||||
}
|
||||
if (this._receivingSuggests) {
|
||||
this.messageReceiveSuggestsEvent.success();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _receiveTotalMessagesEvent = {
|
||||
start: () => {
|
||||
// 打断了
|
||||
this._receiveTotalMessagesReportEvent =
|
||||
this._createReceiveTotalMessagesEvent();
|
||||
},
|
||||
error: (reason: string) => {
|
||||
this._receiveTotalMessagesReportEvent?.addDurationPoint('failed');
|
||||
|
||||
this._receiveTotalMessagesReportEvent?.error({
|
||||
reason,
|
||||
});
|
||||
},
|
||||
success: (allFinish = false) => {
|
||||
this._receiveTotalMessagesReportEvent?.addDurationPoint('success');
|
||||
this._receiveTotalMessagesReportEvent?.success({
|
||||
meta: {
|
||||
reply_has_finished: allFinish,
|
||||
},
|
||||
});
|
||||
},
|
||||
finish: () => {
|
||||
this._receiveTotalMessagesEvent?.success(true);
|
||||
},
|
||||
};
|
||||
|
||||
messageReceiveSuggestsEvent = {
|
||||
start: () => {
|
||||
this._messageReceiveSuggestsEvent =
|
||||
this._createMessageReceiveSuggestsEvent();
|
||||
this._receivingSuggests = true;
|
||||
this._hasReceiveFirstSuggestChunk = false;
|
||||
},
|
||||
receiveSuggest: () => {
|
||||
if (!this._messageReceiveSuggestsEventGate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._hasReceiveFirstSuggestChunk) {
|
||||
this._messageReceiveSuggestsEvent?.addDurationPoint('first');
|
||||
this._hasReceiveFirstSuggestChunk = true;
|
||||
}
|
||||
},
|
||||
success: () => {
|
||||
if (!this._messageReceiveSuggestsEventGate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._messageReceiveSuggestsEvent?.addDurationPoint('success');
|
||||
this._messageReceiveSuggestsEvent?.success({
|
||||
meta: {
|
||||
reply_has_finished: !this._receivingSuggests,
|
||||
},
|
||||
});
|
||||
this._receivingSuggests = false;
|
||||
},
|
||||
finish: () => {
|
||||
if (!this._messageReceiveSuggestsEventGate()) {
|
||||
return;
|
||||
}
|
||||
this.messageReceiveSuggestsEvent.success();
|
||||
this._receiveTotalMessagesEvent.finish();
|
||||
},
|
||||
error: ({ error, reason }: ErrorPayload) => {
|
||||
if (!this._messageReceiveSuggestsEventGate()) {
|
||||
return;
|
||||
}
|
||||
this._messageReceiveSuggestsEvent?.addDurationPoint('failed');
|
||||
this._messageReceiveSuggestsEvent?.error({ error, reason });
|
||||
this._receivingSuggests = false;
|
||||
},
|
||||
};
|
||||
|
||||
receiveMessageEvent = {
|
||||
error: () => {
|
||||
if (!this._receiveMessagesEventGate()) {
|
||||
return;
|
||||
}
|
||||
this._receiveMessagesEvent?.addDurationPoint('failed');
|
||||
|
||||
this._receivingMessages = false;
|
||||
},
|
||||
success: (allFinish = false) => {
|
||||
if (!this._receiveMessagesEventGate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._receiveMessagesEvent?.addDurationPoint('success');
|
||||
this._receiveMessagesEvent?.success({
|
||||
meta: {
|
||||
content_length: this._messageTotalContent,
|
||||
reply_has_finished: allFinish,
|
||||
},
|
||||
});
|
||||
this._receivingMessages = false;
|
||||
},
|
||||
start: () => {
|
||||
this._receiveMessagesEvent = this._createReceiveMessagesEvent();
|
||||
this._receivingMessages = true;
|
||||
this._hasReceiveFirstChunk = false;
|
||||
this._messageTotalContent = 0;
|
||||
this._timer = setTimeout(this.receiveMessageEvent.error, overtime);
|
||||
},
|
||||
receiveMessage: (message: Message<ContentType>) => {
|
||||
if (!this._receiveMessagesEventGate()) {
|
||||
return;
|
||||
}
|
||||
if (!message.content) {
|
||||
// 回复消息为空的错误事件上报
|
||||
reporter.errorEvent({
|
||||
eventName: ReportEventNames.emptyReceiveMessage,
|
||||
error: new CustomError(
|
||||
ReportEventNames.emptyReceiveMessage,
|
||||
message.content || 'empty content',
|
||||
),
|
||||
});
|
||||
}
|
||||
this._messageTotalContent += message.content?.length ?? 0;
|
||||
|
||||
if (this._hasReceiveFirstChunk) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._clearTimeout();
|
||||
this._receiveMessagesEvent?.addDurationPoint('first');
|
||||
this._hasReceiveFirstChunk = true;
|
||||
},
|
||||
|
||||
finish: (message: Message<ContentType>) => {
|
||||
if (!this._receiveMessagesEventGate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.receiveMessageEvent.success(true);
|
||||
if ('ext' in message && hasSuggestion(message.ext)) {
|
||||
this.messageReceiveSuggestsEvent.start();
|
||||
} else {
|
||||
this._receiveTotalMessagesEvent.finish();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
executeDraftBotEvent = {
|
||||
start: () => {
|
||||
this._executeDraftBotEvent = this._createExecuteDraftBotEvent();
|
||||
this.interrupt();
|
||||
},
|
||||
success: () => {
|
||||
this._executeDraftBotEvent?.addDurationPoint('finish');
|
||||
this._executeDraftBotEvent?.success({
|
||||
meta: {
|
||||
...this.getLogID(),
|
||||
},
|
||||
});
|
||||
this._receiveTotalMessagesEvent.start();
|
||||
this.receiveMessageEvent.start();
|
||||
},
|
||||
error: ({ error, reason }: ErrorPayload) => {
|
||||
this._executeDraftBotEvent?.error({
|
||||
error,
|
||||
reason,
|
||||
meta: {
|
||||
...this.getLogID(),
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
start(botID: string) {
|
||||
this.botID = botID;
|
||||
}
|
||||
}
|
||||
|
||||
export const messageReportEvent = new MessageReportEvent();
|
||||
116
frontend/packages/arch/bot-utils/src/number.ts
Normal file
116
frontend/packages/arch/bot-utils/src/number.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ceil } from 'lodash-es';
|
||||
|
||||
export const simpleformatNumber = (num: number | string) =>
|
||||
new Intl.NumberFormat('en-US').format(parseInt(String(num)));
|
||||
|
||||
export const formatBytes = (bytes: number, decimals = 2) => {
|
||||
if (!bytes) {
|
||||
return '0 Byte';
|
||||
}
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
const digit = parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
|
||||
|
||||
return `${digit} ${sizes[i]}`;
|
||||
};
|
||||
const THOUSAND = 1e3;
|
||||
const MILLION = 1e6;
|
||||
const BILLION = 1e9;
|
||||
const TRILLION = 1e12;
|
||||
//将数字转换成K、M等单位
|
||||
export const formatNumber = (num: number) => {
|
||||
const absNum = Math.abs(num);
|
||||
if (absNum >= TRILLION) {
|
||||
return `${ceil(num / TRILLION, 1)}T`;
|
||||
}
|
||||
if (absNum >= BILLION) {
|
||||
return `${ceil(num / BILLION, 1)}B`;
|
||||
}
|
||||
if (absNum >= MILLION) {
|
||||
return `${ceil(num / MILLION, 1)}M`;
|
||||
}
|
||||
if (absNum >= THOUSAND) {
|
||||
return `${ceil(num / THOUSAND, 1)}K`;
|
||||
}
|
||||
return num;
|
||||
};
|
||||
|
||||
// 将数字转换成百分数, 向上取整
|
||||
export const formatPercent = (num?: number): string => {
|
||||
if (num === undefined || num === null) {
|
||||
return 'NaN%';
|
||||
}
|
||||
const percentage = num * 100;
|
||||
|
||||
let formatted = percentage.toFixed(1);
|
||||
|
||||
// 如果小数点后一位是0,则移除小数点和0
|
||||
if (formatted.endsWith('.0')) {
|
||||
formatted = formatted.slice(0, -2);
|
||||
}
|
||||
|
||||
// 添加百分号并返回结果
|
||||
return `${formatted}%`;
|
||||
};
|
||||
|
||||
// 格式化时间, 毫秒, 保留一位小数点
|
||||
// 比如6.7s, 3.2min, 100ms, 1.3h
|
||||
export const formatTime = (ms: number) => {
|
||||
const absMs = Math.abs(ms);
|
||||
|
||||
if (absMs >= 3600000) {
|
||||
const hours = (ms / 3600000).toFixed(1);
|
||||
return hours.endsWith('.0') ? `${hours.slice(0, -2)}h` : `${hours}h`;
|
||||
}
|
||||
|
||||
if (absMs >= 60000) {
|
||||
const minutes = (ms / 60000).toFixed(1);
|
||||
return minutes.endsWith('.0')
|
||||
? `${minutes.slice(0, -2)}min`
|
||||
: `${minutes}min`;
|
||||
}
|
||||
|
||||
if (absMs >= 10000) {
|
||||
const seconds = (ms / 1000).toFixed(1);
|
||||
return seconds.endsWith('.0') ? `${seconds.slice(0, -2)}s` : `${seconds}s`;
|
||||
}
|
||||
|
||||
return `${ms.toFixed(0)}ms`;
|
||||
};
|
||||
|
||||
export const getEllipsisCount = (num: number, max: number): string =>
|
||||
num > max ? `${max}+` : `${num}`;
|
||||
|
||||
/**
|
||||
* @deprecated 不知道这个函数是干啥的。。。
|
||||
*/
|
||||
export const exhaustiveCheck = (_v: never) => {
|
||||
// empty
|
||||
};
|
||||
|
||||
export async function sleep(timer = 3000) {
|
||||
return new Promise<void>(resolve => {
|
||||
setTimeout(() => resolve(), timer);
|
||||
});
|
||||
}
|
||||
73
frontend/packages/arch/bot-utils/src/platform.ts
Normal file
73
frontend/packages/arch/bot-utils/src/platform.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Browser from 'bowser';
|
||||
|
||||
const browser = Browser.getParser(window.navigator.userAgent);
|
||||
|
||||
let getIsMobileCache: boolean | undefined;
|
||||
/**
|
||||
* 是否是移动设备
|
||||
* 注:ipad 不是移动设备
|
||||
*/
|
||||
const isMobile = () => browser.getPlatformType(true).includes('mobile');
|
||||
|
||||
export const getIsMobile = () => {
|
||||
if (typeof getIsMobileCache === 'undefined') {
|
||||
getIsMobileCache = isMobile();
|
||||
}
|
||||
return getIsMobileCache;
|
||||
};
|
||||
|
||||
let getIsIPhoneOrIPadCache: boolean | undefined;
|
||||
/**
|
||||
* gpt-4 提供的代码
|
||||
*/
|
||||
export const getIsIPhoneOrIPad = () => {
|
||||
if (typeof getIsIPhoneOrIPadCache === 'undefined') {
|
||||
const { userAgent } = navigator;
|
||||
const isAppleDevice = /iPad|iPhone|iPod/.test(userAgent);
|
||||
const isIPadOS =
|
||||
userAgent.includes('Macintosh') &&
|
||||
'ontouchstart' in document.documentElement;
|
||||
|
||||
getIsIPhoneOrIPadCache = isAppleDevice || isIPadOS;
|
||||
}
|
||||
|
||||
return getIsIPhoneOrIPadCache;
|
||||
};
|
||||
|
||||
let getIsIPadCache: boolean | undefined;
|
||||
/**
|
||||
* gpt-4 提供的代码
|
||||
*/
|
||||
export const getIsIPad = () => {
|
||||
if (typeof getIsIPadCache === 'undefined') {
|
||||
const { userAgent } = navigator;
|
||||
const isIPadDevice = /iPad/.test(userAgent);
|
||||
const isIPadOS =
|
||||
userAgent.includes('Macintosh') &&
|
||||
'ontouchstart' in document.documentElement;
|
||||
|
||||
getIsIPadCache = isIPadDevice || isIPadOS;
|
||||
}
|
||||
|
||||
return getIsIPadCache;
|
||||
};
|
||||
|
||||
export const getIsMobileOrIPad = () => getIsMobile() || getIsIPhoneOrIPad();
|
||||
|
||||
export const getIsSafari = () => browser.getBrowserName(true) === 'safari';
|
||||
174
frontend/packages/arch/bot-utils/src/post-message-channel.ts
Normal file
174
frontend/packages/arch/bot-utils/src/post-message-channel.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
interface BusinessData<T> {
|
||||
code: number; // 0: 成功, 其他: 错误码,业务tidying
|
||||
data?: T;
|
||||
message?: string;
|
||||
}
|
||||
enum MessageType {
|
||||
REQUEST = 'request',
|
||||
RESPONSE = 'response',
|
||||
}
|
||||
interface MessageChannelEvent<T> {
|
||||
syncNo: number;
|
||||
type: MessageType;
|
||||
senderName?: string;
|
||||
toName?: string;
|
||||
eventName?: string;
|
||||
requestData?: unknown;
|
||||
respondData?: BusinessData<T>;
|
||||
}
|
||||
enum ErrorType {
|
||||
TIMEOUT = -1,
|
||||
UNKNOWN = -2,
|
||||
}
|
||||
|
||||
type DestoryListenerFun = () => void;
|
||||
|
||||
const DEFAULT_TIMEOUT = 3000;
|
||||
|
||||
export class PostMessageChannel {
|
||||
private eventEmitter: EventEmitter = new EventEmitter();
|
||||
private syncEventId: number = Math.ceil(10000 * Math.random());
|
||||
private senderName = '';
|
||||
private toName?: string = '';
|
||||
private targetOrigin = '';
|
||||
private channelPort: Window;
|
||||
private onMessageFunc?: (event: MessageEvent) => void;
|
||||
|
||||
public constructor({
|
||||
channelPort,
|
||||
senderName,
|
||||
toName,
|
||||
targetOrigin = '*',
|
||||
}: {
|
||||
channelPort: Window;
|
||||
senderName: string;
|
||||
toName?: string;
|
||||
targetOrigin?: string;
|
||||
}) {
|
||||
this.channelPort = channelPort;
|
||||
this.senderName = senderName;
|
||||
this.toName = toName;
|
||||
this.targetOrigin = targetOrigin;
|
||||
this.initListner();
|
||||
}
|
||||
|
||||
public destory() {
|
||||
this.onMessageFunc &&
|
||||
window.removeEventListener('message', this.onMessageFunc);
|
||||
this.eventEmitter.removeAllListeners();
|
||||
}
|
||||
public async send<T1, T2>(
|
||||
eventName: string,
|
||||
data: T1,
|
||||
timeout = DEFAULT_TIMEOUT,
|
||||
): Promise<BusinessData<T2>> {
|
||||
const syncNo = this.syncEventId++;
|
||||
|
||||
const messageEvent: MessageChannelEvent<T1> = {
|
||||
syncNo,
|
||||
type: MessageType.REQUEST,
|
||||
senderName: this.senderName,
|
||||
toName: this.toName,
|
||||
eventName,
|
||||
requestData: data,
|
||||
};
|
||||
this.channelPort.postMessage(messageEvent, this.targetOrigin);
|
||||
return await this.awaitRespond(syncNo, timeout);
|
||||
}
|
||||
public onRequest<T1, T2>(
|
||||
eventName: string,
|
||||
callback: (data: T1) => BusinessData<T2> | Promise<BusinessData<T2>>,
|
||||
): DestoryListenerFun {
|
||||
const onHandle = async (event: MessageEvent) => {
|
||||
const messageEvent = event.data as MessageChannelEvent<unknown>;
|
||||
const result = await callback(messageEvent.requestData as T1);
|
||||
const responseMessageEvent: MessageChannelEvent<T2> = {
|
||||
syncNo: messageEvent.syncNo,
|
||||
type: MessageType.RESPONSE,
|
||||
toName: messageEvent.senderName || '',
|
||||
senderName: this.senderName,
|
||||
eventName,
|
||||
respondData: result,
|
||||
};
|
||||
if (event.source) {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
event.source.postMessage(responseMessageEvent, event.origin);
|
||||
} else {
|
||||
this.channelPort.postMessage(responseMessageEvent, this.targetOrigin);
|
||||
}
|
||||
};
|
||||
this.eventEmitter.on(`${MessageType.REQUEST}_${eventName}`, onHandle);
|
||||
return () => {
|
||||
this.eventEmitter.off(`${MessageType.REQUEST}_${eventName}`, onHandle);
|
||||
};
|
||||
}
|
||||
|
||||
private initListner() {
|
||||
this.onMessageFunc = (event: MessageEvent) => {
|
||||
const messageEvent = event.data as MessageChannelEvent<unknown>;
|
||||
|
||||
if (
|
||||
messageEvent.type === MessageType.RESPONSE &&
|
||||
this.senderName === messageEvent.toName
|
||||
) {
|
||||
this.eventEmitter.emit(
|
||||
`${MessageType.RESPONSE}_${messageEvent.syncNo}`,
|
||||
messageEvent,
|
||||
);
|
||||
} else if (
|
||||
messageEvent.type === MessageType.REQUEST &&
|
||||
(!messageEvent.toName || this.senderName === messageEvent.toName)
|
||||
) {
|
||||
this.eventEmitter.emit(
|
||||
`${MessageType.REQUEST}_${messageEvent.eventName}`,
|
||||
event,
|
||||
);
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', this.onMessageFunc);
|
||||
}
|
||||
|
||||
private awaitRespond<T>(syncNo: number, timeout): Promise<BusinessData<T>> {
|
||||
const eventName = `${MessageType.RESPONSE}_${syncNo}`;
|
||||
return new Promise(resolve => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.eventEmitter.emit(eventName, {
|
||||
respondData: {
|
||||
code: ErrorType.TIMEOUT,
|
||||
message: 'timeout',
|
||||
},
|
||||
});
|
||||
}, timeout);
|
||||
this.eventEmitter.once(
|
||||
eventName,
|
||||
(messageEvent: MessageChannelEvent<T>) => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(
|
||||
messageEvent.respondData || {
|
||||
code: ErrorType.UNKNOWN,
|
||||
message: 'unknow error',
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
export enum WidthThresholds {
|
||||
Small = 1280,
|
||||
Medium = 1400,
|
||||
Large = 1600,
|
||||
Default = 1300,
|
||||
}
|
||||
|
||||
export enum ColumnSize {
|
||||
Small = 76,
|
||||
Medium = 136,
|
||||
Large = 160,
|
||||
Default = 80,
|
||||
}
|
||||
|
||||
type MinWidth = 'auto' | number;
|
||||
|
||||
interface WidthRange {
|
||||
threshold: WidthThresholds;
|
||||
columnWidth: ColumnSize;
|
||||
}
|
||||
|
||||
const defaultRange = {
|
||||
hreshold: WidthThresholds.Default,
|
||||
columnWidth: ColumnSize.Default,
|
||||
};
|
||||
|
||||
const colWidthRanges: WidthRange[] = [
|
||||
{ threshold: WidthThresholds.Large, columnWidth: ColumnSize.Large },
|
||||
{ threshold: WidthThresholds.Medium, columnWidth: ColumnSize.Medium },
|
||||
{ threshold: WidthThresholds.Small, columnWidth: ColumnSize.Small },
|
||||
];
|
||||
|
||||
export const responsiveTableColumn = (
|
||||
width: number,
|
||||
minWidth: MinWidth = ColumnSize.Medium,
|
||||
): ColumnSize | string => {
|
||||
if (minWidth === 'auto' || typeof minWidth !== 'number') {
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
// 查找第一个符合条件的项
|
||||
const range =
|
||||
colWidthRanges.find(colWidth => width >= colWidth.threshold) ||
|
||||
defaultRange;
|
||||
|
||||
// 返回 minWidth 或找到的 columnWidth,取决于哪个更大
|
||||
return Math.max(minWidth, range.columnWidth);
|
||||
};
|
||||
38
frontend/packages/arch/bot-utils/src/retry-import.ts
Normal file
38
frontend/packages/arch/bot-utils/src/retry-import.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO: https://github.com/web-infra-dev/rsbuild/issues/91
|
||||
export const retryImport = <T>(
|
||||
importFunction: () => Promise<T>,
|
||||
maxRetryCount = 3,
|
||||
) => {
|
||||
let maxCount = 0;
|
||||
const loadWithRetry = (): Promise<T> =>
|
||||
new Promise((resolve, reject) => {
|
||||
importFunction().then(
|
||||
res => resolve(res),
|
||||
error => {
|
||||
if (maxCount >= maxRetryCount) {
|
||||
reject(error);
|
||||
} else {
|
||||
maxCount++;
|
||||
resolve(loadWithRetry());
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
return loadWithRetry();
|
||||
};
|
||||
53
frontend/packages/arch/bot-utils/src/safe-json-parse.ts
Normal file
53
frontend/packages/arch/bot-utils/src/safe-json-parse.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { REPORT_EVENTS } from '@coze-arch/report-events';
|
||||
import { logger, reporter } from '@coze-arch/logger';
|
||||
|
||||
/**
|
||||
* @deprecated 这其实是 unsafe 的,请换用 typeSafeJSONParse
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const safeJSONParse: (v: any, emptyValue?: any) => any = (
|
||||
v,
|
||||
emptyValue,
|
||||
) => {
|
||||
try {
|
||||
const json = JSON.parse(v);
|
||||
return json;
|
||||
} catch (e) {
|
||||
logger.persist.error({
|
||||
error: e as Error,
|
||||
eventName: REPORT_EVENTS.parseJSON,
|
||||
message: 'parse json fail',
|
||||
});
|
||||
return emptyValue ?? void 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const typeSafeJSONParse = (v: unknown): unknown => {
|
||||
if (typeof v === 'object') {
|
||||
return v;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(String(v));
|
||||
} catch (e) {
|
||||
reporter.errorEvent({
|
||||
error: e as Error,
|
||||
eventName: REPORT_EVENTS.parseJSON,
|
||||
});
|
||||
}
|
||||
};
|
||||
24
frontend/packages/arch/bot-utils/src/skill.ts
Normal file
24
frontend/packages/arch/bot-utils/src/skill.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type SkillKeyEnum } from '@coze-agent-ide/tool-config';
|
||||
|
||||
/**
|
||||
* `能力模块主键` 转 `接口定义的属性名` 函数
|
||||
* ⚠️ 命名需参看 @/services/auto-generate/developer_api/namespaces/developer_api > TabDisplayItems
|
||||
*/
|
||||
export const skillKeyToApiStatusKeyTransformer = ($key: SkillKeyEnum) =>
|
||||
`${$key}_tab_status`;
|
||||
201
frontend/packages/arch/bot-utils/src/upload-file-v2.ts
Normal file
201
frontend/packages/arch/bot-utils/src/upload-file-v2.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
getUploader as initUploader,
|
||||
type CozeUploader,
|
||||
type EventPayloadMaps,
|
||||
} from '@coze-studio/uploader-adapter';
|
||||
import { type GetUploadAuthTokenData } from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { getReportError } from './get-report-error';
|
||||
|
||||
export { type EventPayloadMaps };
|
||||
|
||||
export type UploaderInstance = CozeUploader;
|
||||
|
||||
const removeAllListeners = (instance: UploaderInstance) => {
|
||||
instance.removeAllListeners('stream-progress');
|
||||
instance.removeAllListeners('complete');
|
||||
instance.removeAllListeners('error');
|
||||
instance.removeAllListeners('progress');
|
||||
};
|
||||
|
||||
export interface FileItem {
|
||||
file: File;
|
||||
/**
|
||||
* 非图片的文件 type 为 object
|
||||
* 这里显得很奇怪, 是为了对齐 @byted/uploader 的设计
|
||||
*/
|
||||
fileType: 'image' | 'object';
|
||||
}
|
||||
|
||||
export interface UploadFileV2Param {
|
||||
fileItemList: FileItem[];
|
||||
userId: string;
|
||||
signal: AbortSignal;
|
||||
onProgress?: (event: EventPayloadMaps['progress']) => void;
|
||||
onUploaderReady?: (uploader: UploaderInstance) => void;
|
||||
onUploadError?: (event: EventPayloadMaps['error']) => void;
|
||||
onGetTokenError?: (error: Error) => void;
|
||||
onSuccess?: (event: EventPayloadMaps['complete']) => void;
|
||||
onUploadAllSuccess?: (event: EventPayloadMaps['complete'][]) => void;
|
||||
onStartUpload?: (param: (FileItem & { fileKey: string })[]) => void;
|
||||
onGetUploadInstanceError?: (error: Error) => void;
|
||||
timeout: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 改良版本的上传方法
|
||||
* 1. 能够支持打断, 清除副作用
|
||||
* 2. 更完善的回调函数
|
||||
* 3. 支持一次上传多文件
|
||||
*/
|
||||
// eslint-disable-next-line max-lines-per-function -- 内部的方法分了模块但是都依赖同一个 context 作打断无法拆出去
|
||||
export function uploadFileV2({
|
||||
fileItemList,
|
||||
userId,
|
||||
signal,
|
||||
onProgress,
|
||||
onUploaderReady,
|
||||
onUploadError,
|
||||
onGetTokenError,
|
||||
onSuccess,
|
||||
onUploadAllSuccess,
|
||||
onStartUpload,
|
||||
timeout = 60000,
|
||||
onGetUploadInstanceError,
|
||||
}: UploadFileV2Param) {
|
||||
return new Promise<void>(resolve => {
|
||||
let bytedUploader: UploaderInstance | null = null;
|
||||
|
||||
let stopped = false;
|
||||
|
||||
signal?.addEventListener('abort', () => {
|
||||
bytedUploader?.cancel();
|
||||
if (bytedUploader) {
|
||||
removeAllListeners(bytedUploader);
|
||||
}
|
||||
stopped = true;
|
||||
resolve();
|
||||
});
|
||||
let list: EventPayloadMaps['complete'][] = [];
|
||||
|
||||
const getToken = async () => {
|
||||
try {
|
||||
const dataAuth = await DeveloperApi.GetUploadAuthToken(
|
||||
{
|
||||
scene: 'bot_task',
|
||||
},
|
||||
{ timeout },
|
||||
);
|
||||
const result = dataAuth.data;
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Invalid GetUploadAuthToken Response');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
onGetTokenError?.(getReportError(e).error);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const upload = (authToken: GetUploadAuthTokenData) => {
|
||||
const { service_id, upload_host, auth, schema } =
|
||||
authToken as GetUploadAuthTokenData & { schema?: string };
|
||||
|
||||
const uploader = initUploader(
|
||||
{
|
||||
schema,
|
||||
useFileExtension: true,
|
||||
// 解决报错问题:
|
||||
userId,
|
||||
appId: APP_ID,
|
||||
// cp-disable-next-line
|
||||
imageHost: `https://${upload_host}`, //imageX上传必填
|
||||
imageConfig: {
|
||||
serviceId: service_id || '', // 在视频云中申请的服务id
|
||||
},
|
||||
objectConfig: {
|
||||
serviceId: service_id || '',
|
||||
},
|
||||
imageFallbackHost: IMAGE_FALLBACK_HOST,
|
||||
region: BYTE_UPLOADER_REGION,
|
||||
uploadTimeout: timeout,
|
||||
},
|
||||
IS_OVERSEA,
|
||||
);
|
||||
bytedUploader = uploader;
|
||||
onUploaderReady?.(uploader);
|
||||
|
||||
const fileAndKeyList = fileItemList.map(({ file, fileType }) => {
|
||||
const fileKey = uploader.addFile({
|
||||
file,
|
||||
stsToken: {
|
||||
CurrentTime: auth?.current_time || '',
|
||||
ExpiredTime: auth?.expired_time || '',
|
||||
SessionToken: auth?.session_token || '',
|
||||
AccessKeyId: auth?.access_key_id || '',
|
||||
SecretAccessKey: auth?.secret_access_key || '',
|
||||
},
|
||||
type: fileType, // 上传文件类型,三个可选值:video(视频或者音频,默认值),image(图片),object(普通文件)
|
||||
});
|
||||
return { file, fileType, fileKey };
|
||||
});
|
||||
|
||||
onStartUpload?.(fileAndKeyList);
|
||||
fileAndKeyList.forEach(fileAndKey => {
|
||||
uploader.start(fileAndKey.fileKey);
|
||||
});
|
||||
|
||||
uploader.on('complete', inform => {
|
||||
onSuccess?.(inform as any);
|
||||
|
||||
list.push(inform as any);
|
||||
if (list.length === fileAndKeyList.length) {
|
||||
// 按顺序赋值
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
list = fileAndKeyList.map(({ fileKey }) =>
|
||||
list.find(v => v.key === fileKey),
|
||||
);
|
||||
onUploadAllSuccess?.(list);
|
||||
}
|
||||
});
|
||||
|
||||
uploader.on('error', inform => {
|
||||
onUploadError?.(inform as any);
|
||||
});
|
||||
|
||||
uploader.on('progress', inform => {
|
||||
onProgress?.(inform as any);
|
||||
});
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
const [authData] = await Promise.all([getToken()]);
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
upload(authData);
|
||||
};
|
||||
|
||||
start();
|
||||
});
|
||||
}
|
||||
208
frontend/packages/arch/bot-utils/src/upload-file.ts
Normal file
208
frontend/packages/arch/bot-utils/src/upload-file.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { userStoreService } from '@coze-studio/user-store';
|
||||
import {
|
||||
getUploader as initUploader,
|
||||
type CozeUploader,
|
||||
type Config as BytedUploaderConfig,
|
||||
} from '@coze-studio/uploader-adapter';
|
||||
import { type developer_api } from '@coze-arch/bot-api/developer_api';
|
||||
import { DeveloperApi, workflowApi } from '@coze-arch/bot-api';
|
||||
|
||||
export type BytedUploader = CozeUploader;
|
||||
|
||||
interface Inform {
|
||||
uploadResult: {
|
||||
Uri: string;
|
||||
};
|
||||
extra: string;
|
||||
percent?: number;
|
||||
}
|
||||
|
||||
type BizConfig = Record<
|
||||
string,
|
||||
{
|
||||
getAuthToken: () => Promise<{
|
||||
serviceId: string;
|
||||
uploadHost: string;
|
||||
stsToken: BytedUploaderConfig['stsToken'];
|
||||
schema: string;
|
||||
}>;
|
||||
}
|
||||
>;
|
||||
|
||||
const bizConfig: BizConfig = {
|
||||
bot: {
|
||||
getAuthToken: async () => {
|
||||
const dataAuth = await DeveloperApi.GetUploadAuthToken({
|
||||
scene: 'bot_task',
|
||||
});
|
||||
const dataAuthnr = dataAuth.data;
|
||||
const { service_id, upload_host, auth, schema } = (dataAuthnr ||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{}) as any;
|
||||
|
||||
return {
|
||||
schema,
|
||||
serviceId: service_id || '',
|
||||
uploadHost: upload_host || '',
|
||||
stsToken: {
|
||||
CurrentTime: auth?.current_time || '',
|
||||
ExpiredTime: auth?.expired_time || '',
|
||||
SessionToken: auth?.session_token || '',
|
||||
AccessKeyId: auth?.access_key_id || '',
|
||||
SecretAccessKey: auth?.secret_access_key || '',
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
workflow: {
|
||||
getAuthToken: async () => {
|
||||
const dataAuth = await workflowApi.GetUploadAuthToken({
|
||||
scene: 'imageflow',
|
||||
});
|
||||
const dataAuthnr = dataAuth.data;
|
||||
|
||||
const { service_id, upload_host, auth, schema } = (dataAuthnr ||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{}) as any;
|
||||
|
||||
return {
|
||||
schema,
|
||||
serviceId: service_id || '',
|
||||
uploadHost: upload_host || '',
|
||||
stsToken: {
|
||||
CurrentTime: auth?.current_time || '',
|
||||
ExpiredTime: auth?.expired_time || '',
|
||||
SessionToken: auth?.session_token || '',
|
||||
AccessKeyId: auth?.access_key_id || '',
|
||||
SecretAccessKey: auth?.secret_access_key || '',
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function upLoadFile({
|
||||
biz = 'bot',
|
||||
file,
|
||||
fileType = 'image',
|
||||
getProgress,
|
||||
getUploader,
|
||||
getUploadAuthToken,
|
||||
}: {
|
||||
/** 业务, 不同业务对应不同的 ImageX 服务 */
|
||||
biz?: 'bot' | 'workflow' | string;
|
||||
file: File;
|
||||
fileType?: 'image' | 'object';
|
||||
getProgress?: (progress: number) => void;
|
||||
getUploader?: (uploader: BytedUploader) => void;
|
||||
// 业务方自己获取upload token
|
||||
getUploadAuthToken?: () => Promise<developer_api.GetUploadAuthTokenResponse>;
|
||||
}) {
|
||||
const config = bizConfig[biz];
|
||||
if (!config && !getUploadAuthToken) {
|
||||
throw new Error('upLoadFile need biz');
|
||||
}
|
||||
const result = new Promise<string>((resolve, reject) => {
|
||||
// eslint-disable-next-line complexity
|
||||
(async function () {
|
||||
try {
|
||||
let serviceId, uploadHost, stsToken, schema;
|
||||
if (config) {
|
||||
const data = await config.getAuthToken();
|
||||
serviceId = data.serviceId;
|
||||
uploadHost = data.uploadHost;
|
||||
stsToken = data.stsToken;
|
||||
schema = data.schema;
|
||||
} else if (getUploadAuthToken) {
|
||||
const { data } = await getUploadAuthToken();
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
serviceId = data.service_id;
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
uploadHost = data.upload_host;
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
schema = data.schema;
|
||||
// cp-disable-next-line
|
||||
if (uploadHost.startsWith('https://')) {
|
||||
uploadHost = uploadHost.substr(8);
|
||||
}
|
||||
stsToken = {
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
CurrentTime: data.auth?.current_time || '',
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
ExpiredTime: data.auth?.expired_time || '',
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
SessionToken: data.auth?.session_token || '',
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
AccessKeyId: data.auth?.access_key_id || '',
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
SecretAccessKey: data.auth?.secret_access_key || '',
|
||||
};
|
||||
}
|
||||
|
||||
const bytedUploader: BytedUploader = initUploader(
|
||||
{
|
||||
schema,
|
||||
useFileExtension: true,
|
||||
userId: userStoreService.getUserInfo()?.user_id_str || '',
|
||||
appId: APP_ID,
|
||||
// cp-disable-next-line
|
||||
imageHost: `https://${uploadHost}`, //imageX上传必填
|
||||
imageConfig: {
|
||||
serviceId: serviceId || '', // 在视频云中申请的服务id
|
||||
},
|
||||
objectConfig: {
|
||||
serviceId: serviceId || '',
|
||||
},
|
||||
imageFallbackHost: IMAGE_FALLBACK_HOST,
|
||||
region: BYTE_UPLOADER_REGION,
|
||||
},
|
||||
IS_OVERSEA,
|
||||
);
|
||||
getUploader?.(bytedUploader);
|
||||
bytedUploader.on('complete', inform => {
|
||||
const { uploadResult } = inform;
|
||||
resolve(uploadResult.Uri ?? '');
|
||||
});
|
||||
|
||||
bytedUploader.on('error', inform => {
|
||||
const { extra } = inform;
|
||||
reject(extra);
|
||||
});
|
||||
|
||||
if (getProgress) {
|
||||
bytedUploader.on('progress', inform => {
|
||||
const { percent } = inform as unknown as Inform;
|
||||
getProgress(percent || 0);
|
||||
});
|
||||
}
|
||||
|
||||
const fileKey = bytedUploader.addFile({
|
||||
file,
|
||||
stsToken,
|
||||
type: fileType, // 上传文件类型,三个可选值:video(视频或者音频,默认值),image(图片),object(普通文件)
|
||||
});
|
||||
bytedUploader.start(fileKey);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
49
frontend/packages/arch/bot-utils/src/url.ts
Normal file
49
frontend/packages/arch/bot-utils/src/url.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { getIsMobile, getIsSafari } from './platform';
|
||||
|
||||
export const getParamsFromQuery = (params: { key: string }) => {
|
||||
const { key = '' } = params;
|
||||
const queryParams = queryString.parse(location.search);
|
||||
return (queryParams?.[key] ?? '') as string;
|
||||
};
|
||||
export function appendUrlParam(
|
||||
url: string,
|
||||
key: string,
|
||||
value: string | string[] | null | undefined,
|
||||
) {
|
||||
const urlInfo = queryString.parseUrl(url);
|
||||
if (!value) {
|
||||
delete urlInfo.query[key];
|
||||
} else {
|
||||
urlInfo.query[key] = value;
|
||||
}
|
||||
return queryString.stringifyUrl(urlInfo);
|
||||
}
|
||||
|
||||
export function openUrl(url?: string) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
if (getIsMobile() && getIsSafari()) {
|
||||
location.href = url;
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
37
frontend/packages/arch/bot-utils/src/viewport.ts
Normal file
37
frontend/packages/arch/bot-utils/src/viewport.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const setMobileBody = () => {
|
||||
const bodyStyle = document?.body?.style;
|
||||
const htmlStyle = document?.getElementsByTagName('html')?.[0]?.style;
|
||||
if (bodyStyle && htmlStyle) {
|
||||
bodyStyle.minHeight = '0';
|
||||
htmlStyle.minHeight = '0';
|
||||
bodyStyle.minWidth = '0';
|
||||
htmlStyle.minWidth = '0';
|
||||
}
|
||||
};
|
||||
|
||||
export const setPCBody = () => {
|
||||
const bodyStyle = document?.body?.style;
|
||||
const htmlStyle = document?.getElementsByTagName('html')?.[0]?.style;
|
||||
if (bodyStyle && htmlStyle) {
|
||||
bodyStyle.minHeight = '600px';
|
||||
htmlStyle.minHeight = '600px';
|
||||
bodyStyle.minWidth = '1200px';
|
||||
htmlStyle.minWidth = '1200px';
|
||||
}
|
||||
};
|
||||
67
frontend/packages/arch/bot-utils/tsconfig.build.json
Normal file
67
frontend/packages/arch/bot-utils/tsconfig.build.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/arch/bot-utils/tsconfig.json
Normal file
15
frontend/packages/arch/bot-utils/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
17
frontend/packages/arch/bot-utils/tsconfig.misc.json
Normal file
17
frontend/packages/arch/bot-utils/tsconfig.misc.json
Normal 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
|
||||
}
|
||||
}
|
||||
31
frontend/packages/arch/bot-utils/vitest.config.ts
Normal file
31
frontend/packages/arch/bot-utils/vitest.config.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user