feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -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;
|
||||
@@ -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({}), []);
|
||||
};
|
||||
302
frontend/packages/workflow/playground/src/utils/curl-parser.ts
Normal file
302
frontend/packages/workflow/playground/src/utils/curl-parser.ts
Normal 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 };
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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'],
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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()}`;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
25
frontend/packages/workflow/playground/src/utils/index.ts
Normal file
25
frontend/packages/workflow/playground/src/utils/index.ts
Normal 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';
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user