feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
# @coze-common/chat-area-utils
utils in vanilla ts, no react, no logger, no I18n
## Overview
This package is part of the Coze Studio monorepo and provides utilities functionality. It serves as a core component in the Coze ecosystem.
## Getting Started
### Installation
Add this package to your `package.json`:
```json
{
"dependencies": {
"@coze-common/chat-area-utils": "workspace:*"
}
}
```
Then run:
```bash
rush update
```
### Usage
```typescript
import { /* exported functions/components */ } from '@coze-common/chat-area-utils';
// Example usage
// TODO: Add specific usage examples
```
## Features
- Core functionality for Coze Studio
- TypeScript support
- Modern ES modules
## API Reference
Please refer to the TypeScript definitions for detailed API documentation.
## Development
This package is built with:
- TypeScript
- Modern JavaScript
- Vitest for testing
- ESLint for code quality
## Contributing
This package is part of the Coze Studio monorepo. Please follow the monorepo contribution guidelines.
## License
Apache-2.0

View File

@@ -0,0 +1,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 { describe, expect, vi } from 'vitest';
import { Deferred, sleep } from '../src/async';
it('sleep', async () => {
vi.useFakeTimers();
let count = 0;
sleep(1000).then(() => (count = 1));
vi.runAllTimers();
expect(count).toBe(0);
await Promise.resolve();
expect(count).toBe(1);
});
describe('test deferred', () => {
it('works', async () => {
const deferred = new Deferred<number>();
deferred.resolve(1);
expect(await deferred.promise).toBe(1);
});
it('reject', async () => {
const deferred = new Deferred();
deferred.reject(123);
try {
await deferred.promise;
} catch (err) {
expect(err).toBe(123);
}
});
it('perform like promise', async () => {
const deferred = new Deferred<string>();
deferred.resolve('1');
expect(await deferred).toBe('1');
});
});

View File

@@ -0,0 +1,35 @@
/*
* 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 {
exhaustiveCheckSimple,
exhaustiveCheckForRecord,
} from '../src/exhaustive-check';
it('works', () => {
const obj = { a: 1 };
// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars -- .
const { a, ...rest } = obj;
exhaustiveCheckForRecord(rest);
type N = 1;
const n: N = 1;
switch (n) {
case 1:
break;
default:
exhaustiveCheckSimple(n);
}
});

View File

@@ -0,0 +1,61 @@
/*
* 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 {
sortInt64CompareFn,
getIsDiffWithinRange,
getMinMax,
compareInt64,
getInt64AbsDifference,
} from '../src/int64';
it('正确排序', () => {
expect(['123', '3', '02', '01234'].sort(sortInt64CompareFn)).toMatchObject([
'02',
'3',
'123',
'01234',
]);
});
it('计算两个数字差值小于范围', () => {
expect(getIsDiffWithinRange('1234567', '12345678923456745678', 50)).toBe(
false,
);
expect(
getIsDiffWithinRange('12345678923456745679', '12345678923456745678', 50),
).toBe(true);
});
it('get min max', () => {
expect(getMinMax('1', '3', '2', '5')).toMatchObject({ min: '1', max: '5' });
expect(getMinMax('3', '2', '5')).toMatchObject({ min: '2', max: '5' });
expect(getMinMax('3', '2', '1')).toMatchObject({ min: '1', max: '3' });
expect(getMinMax('3')).toMatchObject({ min: '3', max: '3' });
expect(getMinMax()).toBeNull();
});
it('compare right', () => {
expect(compareInt64('1').greaterThan('0')).toBe(true);
expect(compareInt64('1').lesserThan('10')).toBe(true);
expect(compareInt64('1').eq('1')).toBe(true);
});
it('get diff right', () => {
expect(getInt64AbsDifference('10', '200')).toBe(190);
});

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, it, vi } from 'vitest';
import {
typeSafeJsonParse,
typeSafeJsonParseEnhanced,
} from '../src/json-parse';
it('simple, parse json', () => {
const items = [1, true, [], { a: 1 }];
const empty = () => undefined;
expect(typeSafeJsonParse(JSON.stringify(items[0]), empty)).toBe(1);
expect(typeSafeJsonParse(JSON.stringify(items[1]), empty)).toBe(true);
expect(typeSafeJsonParse(JSON.stringify(items[2]), empty)).toMatchObject(
items[2],
);
expect(typeSafeJsonParse(JSON.stringify(items[3]), empty)).toMatchObject(
items[3],
);
});
it('simple, trigger error', () => {
const onErr = vi.fn();
expect(typeSafeJsonParse('a', onErr)).toBeNull();
expect(onErr.mock.calls.length).toBe(1);
});
it('enhanced, parse json', () => {
const items = [1, true, [], { a: 1 }];
const getEmpty = () => ({
onVerifyError: () => undefined,
onParseError: () => undefined,
verifyStruct: (sth: unknown): sth is unknown => true,
});
expect(
typeSafeJsonParseEnhanced({
str: JSON.stringify(items[0]),
...getEmpty(),
}),
).toBe(1);
expect(
typeSafeJsonParseEnhanced({
str: JSON.stringify(items[1]),
...getEmpty(),
}),
).toBe(true);
expect(
typeSafeJsonParseEnhanced({
str: JSON.stringify(items[2]),
...getEmpty(),
}),
).toMatchObject(items[2]);
expect(
typeSafeJsonParseEnhanced({
str: JSON.stringify(items[3]),
...getEmpty(),
}),
).toMatchObject(items[3]);
});
it('enhanced, trigger parse error', () => {
const onParseError = vi.fn();
expect(
typeSafeJsonParseEnhanced({
str: 'ax',
onParseError,
onVerifyError: () => undefined,
verifyStruct: (sth): sth is unknown => true,
}),
).toBeNull();
expect(onParseError.mock.calls.length).toBe(1);
});
it('enhanced, catch verify not pass error', () => {
const onVerifyError = vi.fn();
expect(
typeSafeJsonParseEnhanced({
str: 'ax',
onParseError: () => undefined,
onVerifyError,
verifyStruct: (sth): sth is unknown => false,
}),
).toBeNull();
expect(onVerifyError.mock.calls.length).toBe(1);
expect(onVerifyError.mock.calls[0][0]).toMatchObject({
message: 'verify struct no pass',
});
});
it('enhanced, catch verify broken', () => {
const onVerifyError = vi.fn();
expect(
typeSafeJsonParseEnhanced({
str: 'ax',
onParseError: () => undefined,
onVerifyError,
verifyStruct: (sth): sth is unknown => {
const obj = Object(null);
return 'x' in obj.x;
},
}),
).toBeNull();
expect(onVerifyError.mock.calls.length).toBe(1);
expect(onVerifyError.mock.calls[0][0]).toBeInstanceOf(TypeError);
expect(onVerifyError.mock.calls[0][0].message).toEqual(
expect.stringContaining("Cannot use 'in' operator"),
);
});

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, it } from 'vitest';
import { performSimpleObjectTypeCheck } from '../src/perform-simple-type-check';
it('check simple obj', () => {
expect(
performSimpleObjectTypeCheck(
{
a: 1,
b: '2',
},
[
['a', 'is-number'],
['b', 'is-string'],
],
),
).toBe(true);
});
it('not block', () => {
expect(performSimpleObjectTypeCheck([], [])).toBe(true);
expect(
performSimpleObjectTypeCheck(
{
a: 1,
},
[],
),
).toBe(true);
expect(
performSimpleObjectTypeCheck(
{
a: 1,
b: '2',
},
[['a', 'is-string']],
),
).toBe(false);
});
it('only check object', () => {
expect(performSimpleObjectTypeCheck(1, [])).toBe(false);
expect(performSimpleObjectTypeCheck('1', [])).toBe(false);
expect(performSimpleObjectTypeCheck(null, [])).toBe(false);
expect(performSimpleObjectTypeCheck(undefined, [])).toBe(false);
});
it('check key exists', () => {
expect(performSimpleObjectTypeCheck({}, [['a', 'is-string']])).toBe(false);
});

View File

@@ -0,0 +1,62 @@
/*
* 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, vi } from 'vitest';
import { RateLimit } from '../src/rate-limit';
it('limit rate', async () => {
vi.useFakeTimers();
const request = vi.fn();
const limiter = new RateLimit(request, {
limit: 3,
onLimitDelay: 1000,
timeWindow: 5000,
});
for (const i of [1, 2, 3, 4, 5]) {
limiter.invoke(i);
}
expect(request.mock.calls.length).toBe(3);
// 1000
await vi.advanceTimersByTimeAsync(1000);
expect(request.mock.calls.length).toBe(4);
// 2000
await vi.advanceTimersByTimeAsync(1000);
expect(request.mock.calls.length).toBe(5);
// 3000
await vi.advanceTimersByTimeAsync(1000);
limiter.invoke();
limiter.invoke();
// 3010
await vi.advanceTimersByTimeAsync(10);
expect(request.mock.calls.length).toBe(6);
// 4010
await vi.advanceTimersByTimeAsync(1000);
expect(request.mock.calls.length).toBe(7);
// 离开窗口
await vi.advanceTimersByTimeAsync(5000);
limiter.invoke();
limiter.invoke();
limiter.invoke();
expect(request.mock.calls.length).toBe(10);
// 进入限流
limiter.invoke();
await vi.advanceTimersByTimeAsync(100);
expect(request.mock.calls.length).toBe(10);
expect((limiter as unknown as { records: number[] }).records.length).toBe(4);
vi.useRealTimers();
});

View File

@@ -0,0 +1,44 @@
/*
* 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, vi } from 'vitest';
import { safeAsyncThrow } from '../src/safe-async-throw';
it('throw in IS_DEV_MODE', () => {
vi.stubGlobal('IS_PROD', true);
vi.stubGlobal('IS_DEV_MODE', true);
expect(() => safeAsyncThrow('1')).toThrow();
});
it('do not throw in BUILD env', () => {
vi.stubGlobal('IS_DEV_MODE', false);
vi.stubGlobal('IS_BOE', false);
vi.stubGlobal('IS_PROD', true);
vi.stubGlobal('window', {
gfdatav1: {
canary: 0,
},
});
vi.useFakeTimers();
safeAsyncThrow('1');
try {
vi.runAllTimers();
} catch (e) {
expect((e as Error).message).toBe('[chat-area] 1');
}
vi.useRealTimers();
});

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, it, vi } from 'vitest';
import { updateOnlyDefined } from '../src/update-only-defined';
it('update only defined', () => {
const updater = vi.fn();
updateOnlyDefined(updater, {
a: undefined,
b: 1,
});
expect(updater.mock.calls[0][0]).toMatchObject({
b: 1,
});
});
it('do not run updater if item value is only undefined', () => {
const updater = vi.fn();
updateOnlyDefined(updater, {
a: undefined,
b: undefined,
});
expect(updater.mock.calls.length).toBe(0);
});

View File

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

View File

@@ -0,0 +1,6 @@
{
"codecov": {
"coverage": 0,
"incrementCoverage": 0
}
}

View File

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

View File

@@ -0,0 +1,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.
*/
export { performSimpleObjectTypeCheck } from './src/perform-simple-type-check';
export { typeSafeJsonParse, typeSafeJsonParseEnhanced } from './src/json-parse';
export { getReportError } from './src/get-report-error';
export { safeAsyncThrow } from './src/safe-async-throw';
export { updateOnlyDefined } from './src/update-only-defined';
export {
sortInt64CompareFn,
getIsDiffWithinRange,
getInt64AbsDifference,
compareInt64,
getMinMax,
compute,
} from './src/int64';
export { type MakeValueUndefinable } from './src/type-helper';
export { sleep, Deferred } from './src/async';
export { flatMapByKeyList } from './src/collection';
export {
exhaustiveCheckForRecord,
exhaustiveCheckSimple,
} from './src/exhaustive-check';
export { RateLimit } from './src/rate-limit';
export { parseMarkdownHelper } from './src/parse-markdown/parse-markdown-to-text';
export {
type Root,
type Link,
type Image,
type Text,
type RootContent,
type Parent,
} from 'mdast';

View File

@@ -0,0 +1,38 @@
{
"name": "@coze-common/chat-area-utils",
"version": "0.0.1",
"description": "utils in vanilla ts, no react, no logger, no I18n",
"license": "Apache-2.0",
"author": "wanglitong@bytedance.com",
"maintainers": [],
"main": "index.ts",
"scripts": {
"build": "exit 0",
"lint": "eslint ./ --cache",
"test": "vitest --run --passWithNoTests",
"test:cov": "npm run test -- --coverage"
},
"dependencies": {
"@coze-arch/bot-env": "workspace:*",
"mdast": "3.0.0-alpha.6"
},
"devDependencies": {
"@coze-arch/bot-md-box-adapter": "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/mdast": "4.0.3",
"@types/node": "^18",
"@vitest/coverage-v8": "~3.0.5",
"big-integer": "^1.6.52",
"lodash-es": "^4.17.21",
"sucrase": "^3.32.0",
"vitest": "~3.0.5"
},
"peerDependencies": {
"lodash-es": "^4.17.21"
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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 sleep = (t = 0) => new Promise(resolve => setTimeout(resolve, t));
export class Deferred<T = void> {
promise: Promise<T>;
resolve!: (value: T) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- .
reject!: (reason?: any) => void;
then: Promise<T>['then'];
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
this.then = this.promise.then.bind(this.promise);
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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 { safeAsyncThrow } from './safe-async-throw';
export const flatMapByKeyList = <T>(
map: Map<string, T>,
arr: string[],
): T[] => {
const res: T[] = [];
for (const key of arr) {
const val = map.get(key);
if (!val) {
safeAsyncThrow(`[flatMapByKeyList] cannot find ${key} in map`);
continue;
}
res.push(val);
}
return res;
};

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 检查没有遗漏的项
*/
export const exhaustiveCheckForRecord = (_: Record<string, never>) => undefined;
export const exhaustiveCheckSimple = (_: never) => undefined;

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isObject } from 'lodash-es';
/**
* @param inputError 传啥都行,一般是 catch (e) 那个 e
* @param reason 多余的解释,我感觉有 eventName 了没啥用
*/
export const getReportError = (
inputError: unknown,
reason?: string,
): {
error: Error;
meta: Record<string, unknown>;
} => {
if (inputError instanceof Error) {
return {
error: inputError,
meta: { reason },
};
}
if (!isObject(inputError)) {
return {
error: new Error(String(inputError)),
meta: { reason },
};
}
return {
error: new Error(''),
meta: { ...covertInputObject(inputError), reason },
};
};
const covertInputObject = (inputError: object) => {
if ('reason' in inputError) {
return {
...inputError,
reasonOfInputError: inputError.reason,
};
}
return inputError;
};

View File

@@ -0,0 +1,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 bigInt, { max, min } from 'big-integer';
export const sortInt64CompareFn = (a: string, b: string) =>
bigInt(a).compare(b);
/** O(1) 遍历 */
export const getMinMax = (...nums: string[]) => {
const num = nums.at(0);
if (num === undefined) {
return null;
}
let minRes = bigInt(num);
let maxRes = bigInt(num);
for (const curStr of nums) {
const cur = bigInt(curStr);
minRes = min(minRes, cur);
maxRes = max(maxRes, cur);
}
return {
min: minRes.toString(),
max: maxRes.toString(),
};
};
export const getIsDiffWithinRange = (a: string, b: string, range: number) => {
const diff = bigInt(a).minus(bigInt(b));
const abs = diff.abs();
return abs.lesser(bigInt(range));
};
export const getInt64AbsDifference = (a: string, b: string) => {
const diff = bigInt(a).minus(bigInt(b));
const abs = diff.abs();
return abs.toJSNumber();
};
export const compareInt64 = (a: string) => {
const bigA = bigInt(a);
return {
greaterThan: (b: string) => bigA.greater(bigInt(b)),
lesserThan: (b: string) => bigA.lesser(bigInt(b)),
eq: (b: string) => bigA.eq(bigInt(b)),
};
};
export const compute = (a: string) => {
const bigA = bigInt(a);
return {
add: (b: string) => bigA.add(b).toString(),
subtract: (b: string) => bigA.subtract(b).toString(),
prev: () => bigA.prev().toString(),
next: () => bigA.next().toString(),
};
};

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const typeSafeJsonParse = (
str: string,
onParseError: (error: Error) => void,
): unknown => {
try {
return JSON.parse(str);
} catch (e) {
onParseError(e as Error);
return null;
}
};
/**
* 泛型类型标注可能需要使用 type 声明,
* refer: https://github.com/microsoft/TypeScript/issues/15300.
*/
export const typeSafeJsonParseEnhanced = <T>({
str,
onParseError,
verifyStruct,
onVerifyError,
}: {
str: string;
onParseError: (error: Error) => void;
/**
* 实现一个类型校验,返回是否通过(boolean);实际上还是靠自觉.
* 可以单独定义, 也可以写作内联 function, 但是注意返回值标注为 predicate,
* refer: https://github.com/microsoft/TypeScript/issues/38390.
*/
verifyStruct: (sth: unknown) => sth is T;
/** 错误原因: 校验崩溃; 校验未通过 */
onVerifyError: (error: Error) => void;
}): T | null => {
const res = typeSafeJsonParse(str, onParseError);
function assertStruct(resLocal: unknown): asserts resLocal is T {
const ok = verifyStruct(resLocal);
if (!ok) {
throw new Error('verify struct no pass');
}
}
try {
assertStruct(res);
return res;
} catch (e) {
onVerifyError(e as Error);
return null;
}
};

View File

@@ -0,0 +1,61 @@
/*
* 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 Text, type Link, type Parent, type Image } from 'mdast';
import { isObject, isUndefined } from 'lodash-es';
/**
* 将markdown转为纯文本
* @param markdown Markdown文本
* @returns string 纯文本
*/
export const getTextFromAst = (ast: unknown): string => {
if (isParent(ast)) {
return `${ast.children.map(child => getTextFromAst(child)).join('')}`;
}
if (isText(ast)) {
return ast.value;
}
if (isLink(ast)) {
return `[${getTextFromAst(ast.children)}](${ast.url})`;
}
if (isImage(ast)) {
return `![${ast.alt}](${ast.url})`;
}
return '';
};
const isParent = (ast: unknown): ast is Parent =>
!!ast && isObject(ast) && 'children' in ast && !isUndefined(ast?.children);
const isLink = (ast: unknown): ast is Link =>
isObject(ast) && 'type' in ast && !isUndefined(ast) && ast.type === 'link';
const isImage = (ast: unknown): ast is Image =>
!isUndefined(ast) && isObject(ast) && 'type' in ast && ast.type === 'image';
const isText = (ast: unknown): ast is Text =>
!isUndefined(ast) && isObject(ast) && 'type' in ast && ast.type === 'text';
export const parseMarkdownHelper = {
isParent,
isLink,
isImage,
isText,
};

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isNumber, isObject, isString } from 'lodash-es';
type CheckMethodName = 'is-string' | 'is-number';
const checkMethodsMap = new Map<CheckMethodName, (sth: unknown) => boolean>([
['is-string', isString],
['is-number', isNumber],
]);
/**
* think about:
* https://www.npmjs.com/package/type-plus
* https://www.npmjs.com/package/generic-type-guard
* https://github.com/runtypes/runtypes
*/
export const performSimpleObjectTypeCheck = <T extends Record<string, unknown>>(
sth: unknown,
pairs: [key: keyof T, checkMethod: CheckMethodName][],
): sth is T => {
if (!isObject(sth)) {
return false;
}
return pairs.every(([k, type]) => {
if (!(k in sth)) {
return false;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- runtime safe
// @ts-expect-error
const val = sth[k];
return checkMethodsMap.get(type)?.(val);
});
};

View File

@@ -0,0 +1,86 @@
/*
* 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 { sleep } from './async';
type Fn<ARGS extends unknown[], Ret = unknown> = (...args: ARGS) => Ret;
/**
* 限流器,对于被限流的异步方法进行以下形式的限流:
* 1. 在 timeWindow 内的前 limit 个请求不做限制,立即发送
* 2. timeWindow 内超过 limit 个请求后,对每个请求依次添加 onLimitDelay 毫秒的延迟
*
* 注意是排队添加,形如 invoke: [1(0ms), 2(0ms), 3(0ms), 4(0ms)]; limit: [1(0ms), 2(0ms), 3(100ms), 4(200ms)]
*
* 另注:这个设计遭到了猛烈抨击,认为 debounce 可以代替掉,实现过于复杂,但是考虑:
* 1. 支持列表双向加载的拉取,简单使用 debounce 可能导致请求某侧丢失;添加延时可以保证不丢失请求
* 2. 列表拉取一旦出现死循环,可能导致恶性问题,如密集地对服务端接口的高频访问
*
* 以上场景通常不应出现,所以 limit 设计也只是对极端场景的兜底,上层 UI 错误理应得到妥善解决
* TODO: wlt - 补充 testcase
*/
export class RateLimit<ARGS extends unknown[], Ret> {
constructor(
private fn: Fn<ARGS, Promise<Ret>>,
private config: {
onLimitDelay: number;
limit: number;
timeWindow: number;
},
) {}
private records: number[] = [];
private getNewInvokeDelay(): number {
const { timeWindow, limit, onLimitDelay } = this.config;
const now = Date.now();
const windowEdge = now - timeWindow;
const idx = this.records.findIndex(t => t >= windowEdge);
if (idx < 0) {
return 0;
}
const lasts = this.records.slice(idx);
if (lasts.length < limit) {
return 0;
}
const last = lasts.at(-1);
if (!last) {
return 0;
}
return last + onLimitDelay - now;
}
private clearRecords() {
const { timeWindow } = this.config;
const now = Date.now();
const windowEdge = now - timeWindow;
const idx = this.records.findLastIndex(t => t < windowEdge);
if (idx >= 0) {
this.records = this.records.slice(idx + 1);
}
}
invoke = async (...args: ARGS): Promise<Ret> => {
const invokeDelay = this.getNewInvokeDelay();
const now = Date.now();
this.records.push(invokeDelay + now);
if (invokeDelay) {
await sleep(invokeDelay);
}
this.clearRecords();
return this.fn(...args);
};
}

View File

@@ -0,0 +1,29 @@
/*
* 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 safeAsyncThrow = (e: string) => {
const err = new Error(`[chat-area] ${e}`);
if (IS_DEV_MODE || IS_BOE) {
throw err;
}
setTimeout(() => {
throw err;
});
};

View File

@@ -0,0 +1,20 @@
/*
* 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-next-line @typescript-eslint/no-explicit-any -- 不知道为啥 unknown 不行,会导致类型转换失败
export type MakeValueUndefinable<T extends Record<string, any>> = {
[k in keyof T]: T[k] | undefined;
};

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types='@coze-arch/bot-typings' />

View File

@@ -0,0 +1,32 @@
/*
* 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 { isUndefined, omitBy } from 'lodash-es';
/**
* zustand update 辅助方法,检查入参对象,丢弃 value 为 undefined 的项.
* zustand 自身没有过滤逻辑,如果类型没有问题,可能意外地将项目置为 undefined 值
*/
export const updateOnlyDefined = <T extends Record<string, unknown>>(
updater: (sth: T) => void,
val: T,
) => {
const left = omitBy(val, isUndefined) as T;
if (!Object.keys(left).length) {
return;
}
updater(left);
};

View File

@@ -0,0 +1,32 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@coze-arch/ts-config/tsconfig.web.json",
"compilerOptions": {
"strictNullChecks": true,
"types": [],
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
},
"include": ["src"],
"references": [
{
"path": "../../../arch/bot-env/tsconfig.build.json"
},
{
"path": "../../../arch/bot-md-box-adapter/tsconfig.build.json"
},
{
"path": "../../../arch/bot-typings/tsconfig.build.json"
},
{
"path": "../../../../config/eslint-config/tsconfig.build.json"
},
{
"path": "../../../../config/ts-config/tsconfig.build.json"
},
{
"path": "../../../../config/vitest-config/tsconfig.build.json"
}
]
}

View File

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

View File

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

View File

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