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,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.
*/
import { logger } from '@coze-arch/logger';
import { type FEATURE_FLAGS } from '../types';
import { PACKAGE_NAMESPACE } from '../constant';
import { nextTick } from './wait';
const PERSIST_CACHE_KEY = 'cache:@coze-arch/bot-flags';
const isFlagsShapeObj = (obj: unknown) => {
if (typeof obj === 'object') {
const shape = obj as FEATURE_FLAGS;
return (
// 如果包含任意属性值不是 boolean则认为不是 flags 对象
Object.keys(shape).some(r => typeof shape[r] !== 'boolean') === false
);
}
return false;
};
export const readFromCache = async (): Promise<FEATURE_FLAGS | undefined> => {
await Promise.resolve(undefined);
const content = window.localStorage.getItem(PERSIST_CACHE_KEY);
if (!content) {
return undefined;
}
try {
const res = JSON.parse(content);
if (isFlagsShapeObj(res)) {
return res;
}
return undefined;
} catch (e) {
return undefined;
}
};
export const saveToCache = async (flags: FEATURE_FLAGS) => {
await nextTick();
try {
if (isFlagsShapeObj(flags)) {
const content = JSON.stringify(flags);
window.localStorage.setItem(PERSIST_CACHE_KEY, content);
}
} catch (e) {
// do nothing
logger.persist.error({
namespace: PACKAGE_NAMESPACE,
message: 'save fg failure',
error: e as Error,
});
}
};

View File

@@ -0,0 +1,36 @@
/*
* 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 FEATURE_FLAGS } from '../types';
export const readFgPromiseFromContext = async (): Promise<
FEATURE_FLAGS | undefined
> => {
const { __fetch_fg_promise__: globalFetchFgPromise } = window;
if (globalFetchFgPromise) {
const res = await globalFetchFgPromise;
return res.data as FEATURE_FLAGS;
}
return undefined;
};
export const readFgValuesFromContext = () => {
const { __fg_values__: globalFgValues } = window;
if (globalFgValues && Object.keys(globalFgValues).length > 0) {
return globalFgValues as FEATURE_FLAGS;
}
return undefined;
};

View File

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

View File

@@ -0,0 +1,132 @@
/*
* 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 { logger } from '@coze-arch/logger';
import type { FEATURE_FLAGS } from '../types';
import { IS_DEV } from '../constant';
import { isEqual } from './tools';
type Interceptor = (key: string) => boolean | undefined;
class FeatureFlagStorage extends EventEmitter {
#proxy: FEATURE_FLAGS | undefined = undefined;
#cache: FEATURE_FLAGS | undefined = undefined;
#inited = false;
#interceptors: Interceptor[] = [];
constructor() {
super();
// fallback
this.#interceptors.push((name: string) => {
const cache = this.#cache;
if (!cache) {
return false;
}
// 从 remote 取值
if (Reflect.has(cache, name)) {
return Reflect.get(cache, name);
}
});
this.#proxy = new Proxy(Object.create(null), {
get: (target, name: string) => {
const cache = this.#cache;
switch (name) {
case 'keys': {
return typeof cache === 'object' ? Reflect.ownKeys(cache) : [];
}
case 'isInited': {
return this.#inited;
}
default: {
return this.#retrieveValueFromInterceptors(name);
}
}
},
set() {
throw new Error('Do not set flag value anytime anyway.');
},
}) as FEATURE_FLAGS;
}
#retrieveValueFromInterceptors(key: string) {
const interceptors = this.#interceptors;
for (const func of interceptors) {
const res = func(key);
if (typeof res === 'boolean') {
return res;
}
}
return false;
}
// has first set FG value
get inited() {
return this.#inited;
}
setFlags(values: FEATURE_FLAGS) {
const cache = this.#cache;
if (isEqual(cache, values)) {
return false;
}
this.#cache = values;
this.#inited = true;
this.notify(values);
return true;
}
notify(values?: FEATURE_FLAGS) {
this.emit('change', values);
}
getFlags(): FEATURE_FLAGS {
if (!this.#inited) {
const error = new Error(
'Trying access feature flag values before the storage been init.',
);
logger.persist.error({ namespace: '@coze-arch/bot-flags', error });
if (IS_DEV) {
throw error;
}
}
return this.#proxy as FEATURE_FLAGS;
}
clear() {
this.#cache = undefined;
this.#inited = false;
}
use(func: Interceptor) {
if (typeof func === 'function') {
this.#interceptors.unshift(func);
} else {
throw new Error('Unexpected retrieve func');
}
}
getPureFlags() {
return this.#cache;
}
}
// singleton
export const featureFlagStorage = new FeatureFlagStorage();

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const isObject = (obj: unknown) => typeof obj === 'object';
export const isEqual = (
obj1: Record<string, boolean> | undefined,
obj2: Record<string, boolean> | undefined,
) => {
// 有任意一个不是对象时,则直接返回 false
if (!isObject(obj1) || !isObject(obj2)) {
return false;
}
const o1 = obj1 as Record<string, boolean>;
const o2 = obj2 as Record<string, boolean>;
// 检查两个对象有相同的键数,如果数量不同,则一定不相等
if (Object.keys(o1).length !== Object.keys(o2).length) {
return false;
}
// 如果键数相同,然后我们检查每个键的值
for (const key in o1) {
// 如果键不存在于第二个对象或者值不同返回false
if (!(key in o2) || o1[key] !== o2[key]) {
return false;
}
}
// 如果所有键都存在于两个对象,并且所有的值都相同,返回 true
return true;
};

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const ONE_SEC = 1000;
export const wait = (ms: number) =>
new Promise(r => {
setTimeout(r, ms);
});
export const nextTick = () => new Promise(r => requestAnimationFrame(r));