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,19 @@
/*
* 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 { useTestsetOptions } from './use-testset-options';
export { useTestsetManageStore } from './use-testset-manage-store';
export { useCheckSchema, SchemaError } from './use-check-schema';

View File

@@ -0,0 +1,196 @@
/*
* 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 { useEffect } from 'react';
import { useState } from 'react';
import { useMemoizedFn } from 'ahooks';
import { logger } from '@coze-arch/logger';
import { ComponentType } from '@coze-arch/bot-api/debugger_api';
import { debuggerApi } from '@coze-arch/bot-api';
import {
type ArrayFieldSchema,
type FormItemSchema,
FormItemSchemaType,
type ObjectFieldSchema,
type NodeFormSchema,
} from '../types';
import { useTestsetManageStore } from './use-testset-manage-store';
export enum SchemaError {
OK = '',
EMPTY = 'empty',
INVALID = 'invalid',
}
/** 变量命名校验规则对齐workflow得参数名校验 */
const PARAM_NAME_VALIDATION_RULE =
/^(?!.*\b(true|false|and|AND|or|OR|not|NOT|null|nil|If|Switch)\b)[a-zA-Z_][a-zA-Z_$0-9]*$/;
function validateParamName(name?: string) {
return Boolean(name && PARAM_NAME_VALIDATION_RULE.test(name));
}
function isArrayOrObjectField(field: FormItemSchema) {
return (
field.type === FormItemSchemaType.LIST ||
field.type === FormItemSchemaType.OBJECT
);
}
function validateArrayOrObjectSchema(
schema?: ObjectFieldSchema | ArrayFieldSchema,
) {
if (!schema) {
return false;
}
if (Array.isArray(schema)) {
const nameSet = new Set<string>();
for (const sub of schema) {
if (!validateParamName(sub.name) || nameSet.has(sub.name)) {
return false;
}
nameSet.add(sub.name);
if (
isArrayOrObjectField(sub) &&
!validateArrayOrObjectSchema(sub.schema)
) {
return false;
}
}
return true;
}
return Boolean(schema.type);
}
function checkArrayOrObjectField(field: FormItemSchema) {
if (!isArrayOrObjectField(field)) {
return true;
}
if (!field.schema) {
return false;
}
if (Array.isArray(field.schema)) {
const nameSet = new Set<string>();
for (const item of field.schema) {
if (!validateParamName(item.name) || nameSet.has(item.name)) {
return false;
}
nameSet.add(item.name);
if (
isArrayOrObjectField(item) &&
!validateArrayOrObjectSchema(item.schema)
) {
return false;
}
}
}
return true;
}
function checkNodeFormSchema(schema: NodeFormSchema) {
// 节点参数为空
if (!schema.inputs.length) {
return false;
}
const nameSet = new Set<string>();
for (const ipt of schema.inputs) {
// 名称非法 or 重复
if (!validateParamName(ipt.name) || nameSet.has(ipt.name)) {
return false;
}
nameSet.add(ipt.name);
// 单独检测复杂类型
if (!checkArrayOrObjectField(ipt)) {
return false;
}
}
return true;
}
function validateSchema(json?: string) {
if (!json) {
return SchemaError.INVALID;
}
try {
const schemas = JSON.parse(json) as NodeFormSchema[];
// schema为空 or start节点的inputs为空
const isEmpty =
schemas.length === 0 ||
(schemas[0].component_type === ComponentType.CozeStartNode &&
schemas[0].inputs.length === 0);
if (isEmpty) {
return SchemaError.EMPTY;
}
for (const schema of schemas) {
if (!checkNodeFormSchema(schema)) {
return SchemaError.INVALID;
}
}
return SchemaError.OK;
} catch (e: any) {
logger.error(e);
return SchemaError.OK;
}
}
/** 检查workflow节点表单是否为空(schema为空 or start节点的inputs为空) */
export function useCheckSchema() {
const { bizComponentSubject, bizCtx } = useTestsetManageStore(store => store);
const [schemaError, setSchemaError] = useState(SchemaError.OK);
const [checking, setChecking] = useState(false);
const checkSchema = useMemoizedFn(async () => {
setChecking(true);
try {
const resp = await debuggerApi.GetSchemaByID({
bizComponentSubject,
bizCtx,
});
const err = validateSchema(resp.schemaJson);
setSchemaError(err);
return err;
} catch (e: any) {
logger.error(e);
setSchemaError(SchemaError.OK);
return SchemaError.OK;
} finally {
setChecking(false);
}
});
return { schemaError, checkSchema, checking };
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useContext } from 'react';
import { useStore } from 'zustand';
import { CustomError } from '@coze-arch/bot-error';
import { type TestsetManageProps } from '../store';
import { TestsetManageContext } from '../context';
export function useTestsetManageStore<T>(
selector: (s: TestsetManageProps) => T,
): T {
const store = useContext(TestsetManageContext);
if (!store) {
throw new CustomError(
'normal_error',
'Missing TestsetManageProvider in the tree',
);
}
return useStore(store, selector);
}

View File

@@ -0,0 +1,112 @@
/*
* 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 { useState } from 'react';
import { useMemoizedFn } from 'ahooks';
import { debuggerApi } from '@coze-arch/bot-api';
import { type TestsetData } from '../types';
import { useTestsetManageStore } from './use-testset-manage-store';
const DEFAULT_PAGE_SIZE = 30;
export interface OptionsData {
list: TestsetData[];
hasNext?: boolean;
nextToken?: string;
}
export function useTestsetOptions() {
const { bizComponentSubject, bizCtx } = useTestsetManageStore(store => store);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [optionsData, setOptionsData] = useState<OptionsData>({ list: [] });
const updateOption = useMemoizedFn((testset?: TestsetData) => {
if (!testset) {
return;
}
const index = optionsData.list.findIndex(
v => v.caseBase?.caseID === testset.caseBase?.caseID,
);
if (index > -1) {
const newList = [...optionsData.list];
newList[index] = testset;
setOptionsData(prev => ({ ...prev, list: newList }));
}
});
const loadOptions = useMemoizedFn(
async (q?: string, limit = DEFAULT_PAGE_SIZE) => {
setLoading(true);
try {
const {
cases = [],
hasNext,
nextToken,
} = await debuggerApi.MGetCaseData({
bizCtx,
bizComponentSubject,
caseName: q,
pageLimit: limit,
});
setOptionsData({ list: cases, hasNext, nextToken });
return cases;
} finally {
setLoading(false);
}
},
);
const loadMoreOptions = useMemoizedFn(
async (q?: string, limit = DEFAULT_PAGE_SIZE) => {
setLoadingMore(true);
try {
const {
cases = [],
hasNext,
nextToken,
} = await debuggerApi.MGetCaseData({
bizCtx,
bizComponentSubject,
caseName: q,
pageLimit: limit,
nextToken: optionsData.nextToken,
});
setOptionsData(prev => ({
list: [...prev.list, ...cases],
hasNext,
nextToken,
}));
return cases;
} finally {
setLoadingMore(false);
}
},
);
return {
loading,
loadOptions,
loadingMore,
loadMoreOptions,
optionsData,
updateOption,
};
}