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,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);
};