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,126 @@
/*
* 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 React, { useLayoutEffect } from 'react';
import { useForceUpdate } from './use-force-update';
/** createSlots is a factory that can create a
* typesafe Slots + Slot pair to use in a component definition
* For example: ActionList.Item uses createSlots to get a Slots wrapper
* + Slot component that is used by LeadingVisual, Description
*/
const createSlots = <SlotNames extends string>(slotNames: SlotNames[]) => {
type Slots = {
[key in SlotNames]?: React.ReactNode;
};
interface ContextProps {
registerSlot: (name: SlotNames, contents: React.ReactNode) => void;
unregisterSlot: (name: SlotNames) => void;
context: Record<string, unknown>;
}
const SlotsContext = React.createContext<ContextProps>({
registerSlot: () => null,
unregisterSlot: () => null,
context: {},
});
// maintain a static reference to avoid infinite render loop
const defaultContext = Object.freeze({});
/** Slots uses a Double render strategy inspired by [reach-ui/descendants](https://github.com/reach/reach-ui/tree/develop/packages/descendants)
* Slot registers themself with the Slots parent.
* When all the children have mounted = registered themselves in slot,
* we re-render the parent component to render with slots
*/
const Slots: React.FC<{
context?: ContextProps['context'];
children: (slots: Slots) => React.ReactNode;
}> = ({ context = defaultContext, children }) => {
// initialise slots
const slotsDefinition: Slots = {};
slotNames.map(name => (slotsDefinition[name] = null));
const slotsRef = React.useRef<Slots>(slotsDefinition);
const rerenderWithSlots = useForceUpdate();
const [isMounted, setIsMounted] = React.useState(false);
// fires after all the effects in children
useLayoutEffect(() => {
rerenderWithSlots();
setIsMounted(true);
}, [rerenderWithSlots]);
const registerSlot = React.useCallback(
(name: SlotNames, contents: React.ReactNode) => {
slotsRef.current[name] = contents;
// don't render until the component mounts = all slots are registered
if (isMounted) {
rerenderWithSlots();
}
},
[isMounted, rerenderWithSlots],
);
// Slot can be removed from the tree as well,
// we need to unregister them from the slot
const unregisterSlot = React.useCallback(
(name: SlotNames) => {
slotsRef.current[name] = null;
rerenderWithSlots();
},
[rerenderWithSlots],
);
/**
* Slots uses a render prop API so abstract the
* implementation detail of using a context provider.
*/
const slots = slotsRef.current;
return (
<SlotsContext.Provider value={{ registerSlot, unregisterSlot, context }}>
{children(slots)}
</SlotsContext.Provider>
);
};
const Slot: React.FC<{
name: SlotNames;
children:
| React.ReactNode
| ((context: ContextProps['context']) => React.ReactNode);
}> = ({ name, children }) => {
const { registerSlot, unregisterSlot, context } =
React.useContext(SlotsContext);
useLayoutEffect(() => {
registerSlot(
name,
typeof children === 'function' ? children(context) : children,
);
return () => unregisterSlot(name);
}, [name, children, registerSlot, unregisterSlot, context]);
return null;
};
return { Slots, Slot };
};
export default createSlots;

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.
*/
// Inspired from reach-ui: https://github.com/reach/reach-ui/blob/develop/packages/utils/src/use-force-update.ts
import React from 'react';
export const useForceUpdate = () => {
const [, rerender] = React.useState({});
return React.useCallback(() => rerender({}), []);
};

View File

@@ -0,0 +1,302 @@
/*
* 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.
*/
/** 直接从 yargs-parser 导出会因为不支持浏览器环境而报错 */
import yargsParser from 'yargs-parser/browser';
import multipart from 'parse-multipart';
import { cloneDeep } from 'lodash-es';
interface ParsedResult {
url: string;
originPath: string;
method: string;
params: Record<string, string>;
headers: Record<string, string>;
query: Record<string, string>;
body: string | Record<string, string> | undefined;
}
class CURLParser {
cURLStr: string;
yargObj: Record<string, string | string[] | Record<string, string> | Buffer>;
constructor(cURLStr: string) {
this.cURLStr = cURLStr;
const yargObj = yargsParser(this.pretreatment(cURLStr));
this.yargObj = yargObj;
}
pretreatment(cURLStr: string) {
if (!cURLStr.startsWith('curl')) {
throw new Error('curl syntax error');
}
// 删除换行
const newLineFound = /\r|\n/.exec(cURLStr);
if (newLineFound) {
cURLStr = cURLStr.replace(/\\\r|\\\n/g, '');
}
// 改成通用写法
cURLStr = cURLStr.replace(/ -XPOST/, ' -X POST');
cURLStr = cURLStr.replace(/ -XGET/, ' -X GET');
cURLStr = cURLStr.replace(/ -XPUT/, ' -X PUT');
cURLStr = cURLStr.replace(/ -XPATCH/, ' -X PATCH');
cURLStr = cURLStr.replace(/ -XDELETE/, ' -X DELETE');
cURLStr = cURLStr.replace(/ --header/g, ' -H');
cURLStr = cURLStr.replace(/ --user-agent/g, ' -A');
cURLStr = cURLStr.replace(/ --request/g, ' -X');
cURLStr = cURLStr.replace(
/ --(data-binary|data-raw|data|data-urlencode)/g,
' -d',
);
cURLStr = cURLStr.replace(/ --form/g, ' -f');
cURLStr = cURLStr.trim();
cURLStr = cURLStr.replace(/^curl/, '');
return cURLStr;
}
/** 如果有误写的两个相同参数,取最后一个 */
getFirstItem(key: string) {
const e = this.yargObj[key];
if (!Array.isArray(e)) {
return e;
}
return e[e.length - 1] || '';
}
transKeyValueArrayToObj(keyValueArray: string[] | string) {
const keyValueObj = {};
let keyValueArr = cloneDeep(keyValueArray);
if (!Array.isArray(keyValueArr)) {
keyValueArr = [keyValueArr] as string[];
}
keyValueArr.forEach((item: string) => {
const arr = item.split('=');
try {
keyValueObj[arr[0]] = JSON.parse(arr[1]);
} catch (error) {
keyValueObj[arr[0]] = arr[1];
}
});
return keyValueObj;
}
getUrl() {
const { yargObj } = this;
let uri = '';
uri = yargObj._[0];
if (yargObj.url) {
uri = yargObj.url as string;
}
if (!uri) {
Object.values(yargObj).forEach(e => {
if (typeof e !== 'string') {
return;
}
if (e.startsWith('http') || e.startsWith('www.')) {
uri = e;
}
});
}
return uri.replace(/['"]+/g, '');
}
getQuery(uri: string) {
const params = {};
try {
const obj = new URL(uri);
if (!obj?.searchParams) {
return params;
}
for (const [key, value] of obj.searchParams) {
params[key] = value;
}
return params;
} catch (error) {
return {};
}
}
getHeaders() {
const { yargObj } = this;
const headers = {};
if (!Reflect.has(yargObj, 'H')) {
return headers;
}
let yargHeaders = yargObj.H;
if (!Array.isArray(yargHeaders)) {
yargHeaders = [yargHeaders] as string[];
}
yargHeaders.forEach((item: string) => {
const i = item.indexOf(':');
const name = item.substring(0, i).trim();
const val = item.substring(i + 1).trim();
headers[name] = val;
});
if (Reflect.has(yargObj, 'A')) {
headers['user-agent'] = this.getFirstItem('A');
}
return headers;
}
getMethods() {
const { yargObj } = this;
let me = this.getFirstItem('X');
if (me) {
return (me as string).toUpperCase();
}
if (Reflect.has(yargObj, 'F')) {
if (!yargObj.F) {
// 存在 -F 参数但为空,则为错误 curl
throw new Error('curl -F params syntax error');
}
return 'POST';
}
if (Reflect.has(yargObj, 'f')) {
if (!yargObj.f) {
// 存在 --form 参数但为空,则为错误 curl
throw new Error('curl --form params syntax error');
}
return 'POST';
}
if (Reflect.has(yargObj, 'd')) {
// 存在 --data 参数,但值为空的场景,默认是 GET 请求
me = !yargObj?.d ? 'GET' : 'POST';
}
return (me ?? ('GET' as string)).toUpperCase();
}
getBody(headers: Record<string, string>) {
const contentType = headers['content-type'] || headers['Content-Type'];
let type = 'Empty';
let data = this.yargObj?.d;
if (contentType) {
if (contentType.indexOf('json') > -1) {
type = 'application/json';
} else if (contentType.indexOf('urlencoded') > -1) {
type = 'application/x-www-form-urlencoded';
} else if (this.cURLStr.indexOf('--data-urlencoded') > -1) {
type = 'application/x-www-form-urlencoded';
} else if (
Array.isArray(data) &&
type !== 'application/x-www-form-urlencoded'
) {
type = 'application/x-www-form-urlencoded';
data = data.join('&');
} else if (contentType.indexOf('form-data') > -1) {
type = 'multipart/form-data';
let boundary = '';
const match = contentType.match(/boundary=.+/);
if (!match) {
type = 'text/plain';
} else {
boundary = match[0].slice(9);
try {
const parts = multipart.Parse(
(data ?? this.yargObj.F ?? '') as Buffer,
boundary,
);
if (parts?.length) {
this.yargObj.F = parts.map(
item => `${item.filename}=${item.data}`,
);
}
} catch (error) {
type = 'text/plain';
}
}
} else if (contentType.indexOf('application/octet-stream') > -1) {
type = 'application/octet-stream';
}
if (this.yargObj.F) {
type = 'multipart/form-data';
}
} else {
// --data "key=value"
const paramsD = this.yargObj?.d;
// -F "file_field=@/path/to/local/file.txt"
// --form 'str="123"'
const paramsF = this.yargObj?.F ?? this.yargObj?.f;
if (typeof paramsF === 'string' && paramsF) {
type = 'multipart/form-data';
} else if (typeof paramsD === 'string' && paramsD) {
try {
JSON.parse(paramsD);
type = 'application/json';
} catch (error) {
// data 不是 json string 时 type 取 form-urlencoded
type = 'application/x-www-form-urlencoded';
}
}
}
let body: string | Record<string, string> | undefined;
const formData = this.yargObj?.f ?? this.yargObj?.F;
const formParamData = this.yargObj?.f;
switch (type) {
case 'application/json':
try {
body = JSON.parse(data as string);
} catch (error) {
body = data as string;
}
break;
case 'application/x-www-form-urlencoded':
if (data) {
try {
const urlSearchParams = new URLSearchParams(data as string);
const params = {};
for (const [key, value] of urlSearchParams) {
params[key] = value;
}
body = params;
} catch (error) {
body = data as string;
}
} else if (formParamData) {
body = this.transKeyValueArrayToObj(
formParamData as string[] | string,
);
}
break;
case 'multipart/form-data':
if (formData) {
body = this.transKeyValueArrayToObj(formData as string[] | string);
}
break;
case 'application/octet-stream':
body = data as string;
break;
default:
body = undefined;
break;
}
const requestBody = {
type,
data: body,
};
return requestBody;
}
parse() {
const uri = this.getUrl();
const headers = this.getHeaders();
const method = this.getMethods();
const obj = new URL(uri);
const res: ParsedResult = {
url: uri,
originPath: obj?.origin + obj?.pathname,
params: {},
method,
headers,
query: this.getQuery(uri),
body: this.getBody(headers) as Record<string, string>,
};
return res;
}
}
export { CURLParser, ParsedResult };

View 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.
*/
export function getCanvasOffset() {
const canvasDOM = document.querySelector('.gedit-flow-background-layer');
const canvasRect = canvasDOM?.getBoundingClientRect();
return { x: canvasRect?.x ?? 0, y: canvasRect?.y ?? 0 };
}

View File

@@ -0,0 +1,25 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import { type EditorProps } from '@coze-workflow/code-editor-adapter';
export const getIDERegionParams = () => ({
region: IS_BOE
? ('boe' as EditorProps['region'])
: (REGION as EditorProps['region']),
locale: (I18n.language === 'en' ? 'en' : 'zh') as EditorProps['locale'],
});

View File

@@ -0,0 +1,78 @@
/*
* 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 WorkflowNodeJSON,
type WorkflowJSON,
} from '@flowgram-adapter/free-layout-editor';
import { WorkflowMode } from '@coze-workflow/base';
function getIsInitStartOuputs(
initStartNode: WorkflowNodeJSON,
flowMode: WorkflowMode,
) {
if (flowMode === WorkflowMode.Workflow) {
return (
initStartNode?.data?.outputs?.length === 1 ||
(initStartNode?.data?.outputs?.length === 2 &&
!initStartNode.data.outputs[1]?.name &&
initStartNode.data.outputs[1]?.required &&
initStartNode.data.outputs[1].type === 'string' &&
!initStartNode.data.outputs[1]?.assistType &&
!initStartNode.data.outputs[1]?.description)
);
} else if (flowMode === WorkflowMode.ChatFlow) {
return (
initStartNode?.data?.outputs?.length === 2 &&
initStartNode.data.outputs[0]?.name === 'USER_INPUT' &&
initStartNode.data.outputs[1]?.name === 'CONVERSATION_NAME'
);
}
}
/**
*
* 判断当前工作流是否是初始状态
* - 节点数量为 2
* - 没有边
* - 开始节点只有一个输入 或者 有两个输入,但第二个输入参数为默认状态
* - 结束节点只有一个有效输出 或者 表单配置了多个输出变量,但都为默认状态
*/
export const getIsInitWorkflow = (
workflowShcmaJson: WorkflowJSON,
flowMode: WorkflowMode,
) => {
if (!workflowShcmaJson) {
return false;
}
const isInitNodesNum = workflowShcmaJson.nodes?.length === 2;
if (!isInitNodesNum) {
return false;
}
const isHasEdge = workflowShcmaJson.edges?.length;
if (isHasEdge) {
return false;
}
const initStartNode = workflowShcmaJson.nodes[0];
const initEndNode = workflowShcmaJson.nodes[1];
const isInitStartOuputs = getIsInitStartOuputs(initStartNode, flowMode);
const isInitStart = !initStartNode?.edges && isInitStartOuputs;
const isInitEnd =
!initEndNode?.edges &&
initEndNode?.data?.inputs?.inputParameters?.length === 1;
return isInitStart && isInitEnd;
};

View 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 { WORKFLOW_INNER_SIDE_SHEET_HOLDER } from '../constants';
export const getWorkflowInnerSideSheetHolder = () => {
const workflowContent = document.querySelector<HTMLElement>(
`#${WORKFLOW_INNER_SIDE_SHEET_HOLDER}`,
);
if (workflowContent) {
return workflowContent;
} else {
return document.body;
}
};

View 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 { WORKFLOW_OUTER_SIDE_SHEET_HOLDER } from '../constants';
export const getWorkflowOuterSideSheetHolder = () => {
const workflowContent = document.querySelector<HTMLElement>(
`#${WORKFLOW_OUTER_SIDE_SHEET_HOLDER}`,
);
if (workflowContent) {
return workflowContent;
} else {
return document.body;
}
};

View 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.
*/
const workflowPath = 'work_flow';
/**
* 获取 Workflow 页面 url
* @param params 相关参数
* @returns Workflow 页面 url
*/
export const getWorkflowUrl = (params: {
space_id: string;
workflow_id: string;
version?: string;
}) => {
const urlParams = new URLSearchParams(params);
return `/${workflowPath}?${urlParams.toString()}`;
};

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 { PluginType } from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
/**
* 根据 workflow 的 pluginId 获取 workflow 的版本号
*/
export const getWorkflowVersionByPluginId = async ({
spaceId,
pluginId,
}: {
spaceId: string;
pluginId?: string;
}) => {
if (!pluginId || pluginId === '0') {
return;
}
const resp = await PluginDevelopApi.GetPlaygroundPluginList(
{
space_id: spaceId,
page: 1,
size: 1,
plugin_ids: [pluginId],
plugin_types: [PluginType.WORKFLOW, PluginType.IMAGEFLOW],
},
{
__disableErrorToast: true,
},
);
// 补全版本信息
const versionName = resp.data?.plugin_list?.[0]?.version_name;
return versionName;
};

View File

@@ -0,0 +1,25 @@
/*
* 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 {
isPluginApiNodeTemplate,
isPluginCategoryNodeTemplate,
isNodeTemplate,
isSubWorkflowNodeTemplate,
} from './node-template';
export { CURLParser, ParsedResult } from './curl-parser';
export { getCanvasOffset } from './get-canvas-offset';
export { getWorkflowVersionByPluginId } from './get-workflow-version';

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 { reporter } from '@coze-arch/logger';
const timerMap: Record<string, { timer?: NodeJS.Timeout; start: number }> = {};
export function moveTimeConsuming(
workflowId: string,
nodeId: string,
wait = 100,
) {
const key = `${workflowId}&&${nodeId}`;
if (timerMap[key]) {
clearTimeout(timerMap[key].timer);
} else {
timerMap[key] = {
timer: undefined,
start: Date.now(),
};
}
timerMap[key].timer = setTimeout(() => {
reporter.event({
eventName: 'workflow_node_drag_consuming',
namespace: 'workflow',
scope: 'node',
meta: {
workflowId,
nodeId,
time: Date.now() - timerMap[key].start,
},
});
delete timerMap[key];
}, wait);
}

View File

@@ -0,0 +1,50 @@
/*
* 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 { get } from 'lodash-es';
import { StandardNodeType } from '@coze-workflow/base';
import {
type UnionNodeTemplate,
type NodeTemplate,
type PluginApiNodeTemplate,
type PluginCategoryNodeTemplate,
type SubWorkflowNodeTemplate,
} from '@/typing';
export const isPluginApiNodeTemplate = (
nodeTemplate: unknown,
): nodeTemplate is PluginApiNodeTemplate =>
Boolean(get(nodeTemplate, 'nodeJSON')) &&
get(nodeTemplate, 'type') === StandardNodeType.Api;
export const isPluginCategoryNodeTemplate = (
nodeTemplate: unknown,
): nodeTemplate is PluginCategoryNodeTemplate =>
Boolean(get(nodeTemplate, 'categoryInfo'));
export const isSubWorkflowNodeTemplate = (
nodeTemplate: unknown,
): nodeTemplate is SubWorkflowNodeTemplate =>
Boolean(get(nodeTemplate, 'nodeJSON')) &&
get(nodeTemplate, 'type') === StandardNodeType.SubWorkflow;
export const isNodeTemplate = (
nodeTemplate: UnionNodeTemplate,
): nodeTemplate is NodeTemplate =>
!isPluginApiNodeTemplate(nodeTemplate) &&
!isPluginCategoryNodeTemplate(nodeTemplate) &&
!isSubWorkflowNodeTemplate(nodeTemplate);

View File

@@ -0,0 +1,71 @@
/*
* 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 class PromiseLimiter<T> {
private concurrency: number;
private activeCount: number;
private enable: boolean;
constructor(concurrency: number, enable = true) {
this.concurrency = concurrency;
this.pendingPromises = [];
this.activeCount = 0;
this.enable = enable;
}
private pendingPromises: Array<{
promiseFactory: () => Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: string) => void;
}>;
run(promiseFactory: () => Promise<T>): Promise<T> {
if (!this.enable) {
return promiseFactory();
}
return new Promise<T>((resolve, reject) => {
this.pendingPromises.push({ promiseFactory, resolve, reject });
this.next();
});
}
private next() {
if (this.activeCount < this.concurrency) {
const item = this.pendingPromises.shift();
if (!item) {
return;
}
const { promiseFactory, resolve, reject } = item;
this.activeCount++;
promiseFactory()
.then(result => {
resolve(result);
})
.catch(error => {
reject(error);
})
.finally(() => {
this.activeCount--;
this.next();
});
}
}
}