Explorar el Código

feat(demo): init http in free demo (#539)

* feat: 初始化 http 节点

* feat(demo): init http
Yiwei Mao hace 6 meses
padre
commit
d93bc79b53

+ 5 - 0
apps/demo-free-layout/src/assets/icon-http.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
+  <path fill-rule="evenodd" clip-rule="evenodd"
+    d="M20.006 4C22.145 4 23.9853 7.39855 24.7651 12.241H15.2469C16.0267 7.39855 17.867 4 20.006 4ZM15.021 20.9119C14.908 19.8023 14.8424 18.6486 14.8424 17.4436C14.8424 16.2421 14.908 15.0848 15.021 13.9752H24.9837C25.0966 15.0848 25.1623 16.2421 25.1623 17.4436C25.1623 18.645 25.0966 19.7987 24.9837 20.9119H15.021ZM23.8044 4.56199C27.6525 5.71199 30.7644 8.55942 32.3022 12.2409H26.4936C26.0199 9.15463 25.1162 6.39537 23.8044 4.56199ZM16.1971 4.56199C12.3563 5.71199 9.23701 8.55942 7.70652 12.2409H13.5151C13.9815 9.15463 14.8852 6.39537 16.1971 4.56199ZM26.7119 13.9752H32.8776C33.1691 15.0848 33.3368 16.2421 33.3368 17.4436C33.3368 18.645 33.1691 19.7987 32.874 20.9119H26.7119C26.8249 19.7766 26.8906 18.6083 26.8906 17.4436C26.8906 16.2789 26.8249 15.1142 26.7119 13.9752ZM13.122 17.4436C13.122 16.2789 13.1876 15.1105 13.3006 13.9752H7.13127C6.83975 15.0885 6.66848 16.2421 6.66848 17.4436C6.66848 18.645 6.83975 19.8023 7.13127 20.912H13.2933C13.1876 19.7767 13.122 18.6119 13.122 17.4436ZM4 25.3373C4 23.8005 5.24582 22.5547 6.78261 22.5547H33.2174C34.7542 22.5547 36 23.8005 36 25.3373V33.2174C36 34.7542 34.7542 36 33.2174 36H6.78261C5.24582 36 4 34.7542 4 33.2174V25.3373ZM10.9109 28.1569H8.48666V25.9161H6.66848V32.6388H8.48666V29.8376H10.9109V32.6388H12.7291V25.9161H10.9109V28.1569ZM13.9412 27.5968H15.7594V32.6388H17.5776V27.5968H19.3958V25.9161H13.9412V27.5968ZM20.6079 27.5968H22.426V32.6388H24.2442V27.5968H26.0625V25.9161H20.6079V27.5968ZM31.5169 25.9161H27.2746V32.6388H29.0927V30.3979H31.5169C32.5472 30.3979 33.3351 29.6696 33.3351 28.7172V27.5968C33.3351 26.6445 32.5472 25.9161 31.5169 25.9161ZM31.5169 28.7172H29.0927V27.5968H31.5169V28.7172Z"
+    fill="#3370FF" />
+</svg>

BIN
apps/demo-free-layout/src/assets/icon-script.png


+ 1 - 1
apps/demo-free-layout/src/components/sidebar/sidebar-renderer.tsx

@@ -92,7 +92,7 @@ export const SidebarRenderer = () => {
       onCancel={handleClose}
       closable={false}
       motion={false}
-      width={368}
+      width={400}
       headerStyle={{
         display: 'none',
       }}

+ 8 - 3
apps/demo-free-layout/src/form-components/form-item/index.tsx

@@ -15,11 +15,12 @@ const { Text } = Typography;
 interface FormItemProps {
   children: React.ReactNode;
   name: string;
-  type: string;
+  type?: string;
   required?: boolean;
   description?: string;
   labelWidth?: number;
   vertical?: boolean;
+  style?: React.CSSProperties;
 }
 export function FormItem({
   children,
@@ -29,14 +30,15 @@ export function FormItem({
   type,
   labelWidth,
   vertical,
+  style,
 }: FormItemProps): JSX.Element {
   const renderTitle = useCallback(
     (showTooltip?: boolean) => (
       <div style={{ width: '0', display: 'flex', flex: '1' }}>
         <Text style={{ width: '100%' }} ellipsis={{ showTooltip: !!showTooltip }}>
           {name}
+          {required && <span style={{ color: '#f93920', paddingLeft: '2px' }}>*</span>}
         </Text>
-        {required && <span style={{ color: '#f93920', paddingLeft: '2px' }}>*</span>}
       </div>
     ),
     []
@@ -56,6 +58,7 @@ export function FormItem({
               justifyContent: 'center',
               alignItems: 'center',
             }),
+        ...style,
       }}
     >
       <div
@@ -64,13 +67,15 @@ export function FormItem({
           alignItems: 'center',
           color: 'var(--semi-color-text-0)',
           width: labelWidth || 118,
+          minWidth: labelWidth || 118,
+          maxWidth: labelWidth || 118,
           position: 'relative',
           display: 'flex',
           columnGap: 4,
           flexShrink: 0,
         }}
       >
-        <TypeTag className="form-item-type-tag" type={type} />
+        {type && <TypeTag className="form-item-type-tag" type={type} />}
         {description ? <Tooltip content={description}>{renderTitle()}</Tooltip> : renderTitle(true)}
       </div>
 

+ 55 - 24
apps/demo-free-layout/src/initial-data.ts

@@ -12,8 +12,8 @@ export const initialData: FlowDocumentJSON = {
       type: 'start',
       meta: {
         position: {
-          x: 186.39660158249967,
-          y: 381.75,
+          x: 180,
+          y: 573.7,
         },
       },
       data: {
@@ -52,8 +52,8 @@ export const initialData: FlowDocumentJSON = {
       type: 'condition',
       meta: {
         position: {
-          x: 640,
-          y: 318.25,
+          x: 1100,
+          y: 510.20000000000005,
         },
       },
       data: {
@@ -91,8 +91,8 @@ export const initialData: FlowDocumentJSON = {
       type: 'end',
       meta: {
         position: {
-          x: 2489.2950705293442,
-          y: 381.75,
+          x: 3008,
+          y: 573.7,
         },
       },
       data: {
@@ -112,8 +112,8 @@ export const initialData: FlowDocumentJSON = {
       type: 'comment',
       meta: {
         position: {
-          x: 640,
-          y: 573.96875,
+          x: 180,
+          y: 756.7,
         },
       },
       data: {
@@ -129,8 +129,8 @@ export const initialData: FlowDocumentJSON = {
       type: 'group',
       meta: {
         position: {
-          x: 163.32056949283722,
-          y: -76.50012170998413,
+          x: 1644,
+          y: 730.1999999999999,
         },
       },
       data: {},
@@ -140,8 +140,8 @@ export const initialData: FlowDocumentJSON = {
           type: 'llm',
           meta: {
             position: {
-              x: 1177.8341013824886,
-              y: 9.249999999999977,
+              x: 180,
+              y: 0,
             },
           },
           data: {
@@ -217,8 +217,8 @@ export const initialData: FlowDocumentJSON = {
           type: 'llm',
           meta: {
             position: {
-              x: 1621.3675909579388,
-              y: 19.24999999999997,
+              x: 640,
+              y: 10,
             },
           },
           data: {
@@ -311,8 +311,8 @@ export const initialData: FlowDocumentJSON = {
       type: 'loop',
       meta: {
         position: {
-          x: 1451.8161064396056,
-          y: 384.9037102954011,
+          x: 1480,
+          y: 90,
         },
       },
       data: {
@@ -328,8 +328,8 @@ export const initialData: FlowDocumentJSON = {
           type: 'llm',
           meta: {
             position: {
-              x: -110.10677817900246,
-              y: 182.98973079191808,
+              x: 344,
+              y: 0,
             },
           },
           data: {
@@ -405,8 +405,8 @@ export const initialData: FlowDocumentJSON = {
           type: 'llm',
           meta: {
             position: {
-              x: 332.31739662589257,
-              y: 182.98973079191802,
+              x: 804,
+              y: 0,
             },
           },
           data: {
@@ -482,8 +482,8 @@ export const initialData: FlowDocumentJSON = {
           type: 'block-start',
           meta: {
             position: {
-              x: -404.5309529838977,
-              y: 346.08973079191816,
+              x: 31.999999999999943,
+              y: 163.1,
             },
           },
           data: {},
@@ -493,8 +493,8 @@ export const initialData: FlowDocumentJSON = {
           type: 'block-end',
           meta: {
             position: {
-              x: 626.7415714307878,
-              y: 346.08973079191793,
+              x: 1116,
+              y: 163.1,
             },
           },
           data: {},
@@ -515,10 +515,41 @@ export const initialData: FlowDocumentJSON = {
         },
       ],
     },
+    {
+      id: 'http_rDGIH',
+      type: 'http',
+      meta: {
+        position: {
+          x: 640,
+          y: 511.20000000000005,
+        },
+      },
+      data: {
+        title: 'HTTP_1',
+        outputs: {
+          type: 'object',
+          properties: {
+            body: {
+              type: 'string',
+            },
+            headers: {
+              type: 'object',
+            },
+            statusCode: {
+              type: 'integer',
+            },
+          },
+        },
+      },
+    },
   ],
   edges: [
     {
       sourceNodeID: 'start_0',
+      targetNodeID: 'http_rDGIH',
+    },
+    {
+      sourceNodeID: 'http_rDGIH',
       targetNodeID: 'condition_0',
     },
     {

+ 1 - 0
apps/demo-free-layout/src/nodes/constants.ts

@@ -7,6 +7,7 @@ export enum WorkflowNodeType {
   Start = 'start',
   End = 'end',
   LLM = 'llm',
+  HTTP = 'http',
   Condition = 'condition',
   Loop = 'loop',
   BlockStart = 'block-start',

+ 60 - 0
apps/demo-free-layout/src/nodes/http/components/api.tsx

@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { Field } from '@flowgram.ai/free-layout-editor';
+import { IFlowTemplateValue, PromptEditorWithVariables } from '@flowgram.ai/form-materials';
+import { Select } from '@douyinfe/semi-ui';
+
+import { useNodeRenderContext } from '../../../hooks';
+import { FormItem } from '../../../form-components';
+
+export function Api() {
+  const { readonly } = useNodeRenderContext();
+
+  return (
+    <div>
+      <FormItem name="API" required vertical>
+        <div style={{ display: 'flex', gap: 5 }}>
+          <Field<string> name="api.method" defaultValue="GET">
+            {({ field }) => (
+              <Select
+                value={field.value}
+                onChange={(value) => {
+                  field.onChange(value as string);
+                }}
+                style={{ width: 85, maxWidth: 85, minWidth: 85 }}
+                size="small"
+                disabled={readonly}
+                optionList={[
+                  { label: 'GET', value: 'GET' },
+                  { label: 'POST', value: 'POST' },
+                  { label: 'PUT', value: 'PUT' },
+                  { label: 'DELETE', value: 'DELETE' },
+                  { label: 'PATCH', value: 'PATCH' },
+                  { label: 'HEAD', value: 'HEAD' },
+                ]}
+              />
+            )}
+          </Field>
+
+          <Field<IFlowTemplateValue> name="api.url">
+            {({ field }) => (
+              <PromptEditorWithVariables
+                disableMarkdownHighlight
+                readonly={readonly}
+                style={{ flexGrow: 1 }}
+                placeholder="Input URL, use var by '{'"
+                value={field.value}
+                onChange={(value) => {
+                  field.onChange(value!);
+                }}
+              />
+            )}
+          </Field>
+        </div>
+      </FormItem>
+    </div>
+  );
+}

+ 94 - 0
apps/demo-free-layout/src/nodes/http/components/body.tsx

@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { Field } from '@flowgram.ai/free-layout-editor';
+import {
+  IFlowTemplateValue,
+  JsonEditorWithVariables,
+  PromptEditorWithVariables,
+} from '@flowgram.ai/form-materials';
+import { Select } from '@douyinfe/semi-ui';
+
+import { useNodeRenderContext } from '../../../hooks';
+import { FormItem } from '../../../form-components';
+
+const BODY_TYPE_OPTIONS = [
+  {
+    label: 'None',
+    value: 'none',
+  },
+  {
+    label: 'JSON',
+    value: 'JSON',
+  },
+  {
+    label: 'Raw Text',
+    value: 'raw-text',
+  },
+];
+
+export function Body() {
+  const { readonly } = useNodeRenderContext();
+
+  const renderBodyEditor = (bodyType: string) => {
+    switch (bodyType) {
+      case 'JSON':
+        return (
+          <Field<IFlowTemplateValue> name="body.json">
+            {({ field }) => (
+              <JsonEditorWithVariables
+                value={field.value?.content}
+                readonly={readonly}
+                activeLinePlaceholder="use var by '@'"
+                onChange={(value) => {
+                  field.onChange({ type: 'template', content: value });
+                }}
+              />
+            )}
+          </Field>
+        );
+      case 'raw-text':
+        return (
+          <Field<IFlowTemplateValue> name="body.rawText">
+            {({ field }) => (
+              <PromptEditorWithVariables
+                disableMarkdownHighlight
+                readonly={readonly}
+                style={{ flexGrow: 1 }}
+                placeholder="Input raw text, use var by '{'"
+                onChange={(value) => {
+                  field.onChange(value!);
+                }}
+              />
+            )}
+          </Field>
+        );
+      default:
+        return null;
+    }
+  };
+
+  return (
+    <Field<string> name="body.bodyType" defaultValue="JSON">
+      {({ field }) => (
+        <div style={{ marginTop: 5 }}>
+          <FormItem name="Body" vertical>
+            <Select
+              value={field.value}
+              onChange={(value) => {
+                field.onChange(value as string);
+              }}
+              style={{ width: '100%', marginBottom: 10 }}
+              disabled={readonly}
+              size="small"
+              optionList={BODY_TYPE_OPTIONS}
+            />
+            {renderBodyEditor(field.value)}
+          </FormItem>
+        </div>
+      )}
+    </Field>
+  );
+}

+ 8 - 0
apps/demo-free-layout/src/nodes/http/components/headers.tsx

@@ -0,0 +1,8 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+export function Headers() {
+  return <div>headers</div>;
+}

+ 8 - 0
apps/demo-free-layout/src/nodes/http/components/params.tsx

@@ -0,0 +1,8 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+export function Params() {
+  return <div>params</div>;
+}

+ 51 - 0
apps/demo-free-layout/src/nodes/http/components/timeout.tsx

@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { Field } from '@flowgram.ai/free-layout-editor';
+import { InputNumber } from '@douyinfe/semi-ui';
+
+import { useNodeRenderContext } from '../../../hooks';
+import { FormItem } from '../../../form-components';
+
+export function Timeout() {
+  const { readonly } = useNodeRenderContext();
+
+  return (
+    <div>
+      <FormItem name="Timeout(ms)" required style={{ flex: 1 }}>
+        <Field<number> name="timeout.timeout" defaultValue={10000}>
+          {({ field }) => (
+            <InputNumber
+              size="small"
+              value={field.value}
+              onChange={(value) => {
+                field.onChange(value as number);
+              }}
+              disabled={readonly}
+              style={{ width: '100%' }}
+              min={0}
+            />
+          )}
+        </Field>
+      </FormItem>
+      <FormItem name="Retry Times" required>
+        <Field<number> name="timeout.retryTimes" defaultValue={1}>
+          {({ field }) => (
+            <InputNumber
+              size="small"
+              value={field.value}
+              onChange={(value) => {
+                field.onChange(value as number);
+              }}
+              disabled={readonly}
+              style={{ width: '100%' }}
+              min={0}
+            />
+          )}
+        </Field>
+      </FormItem>
+    </div>
+  );
+}

+ 27 - 0
apps/demo-free-layout/src/nodes/http/form-render.tsx

@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { FormRenderProps } from '@flowgram.ai/free-layout-editor';
+import { Divider } from '@douyinfe/semi-ui';
+
+import { FormHeader, FormContent, FormOutputs } from '../../form-components';
+import { HTTPNodeJSON } from './types';
+import { Timeout } from './components/timeout';
+import { Body } from './components/body';
+import { Api } from './components/api';
+
+export const FormRender = ({ form }: FormRenderProps<HTTPNodeJSON>) => (
+  <>
+    <FormHeader />
+    <FormContent>
+      <Api />
+      <Divider />
+      <Body />
+      <Divider />
+      <Timeout />
+      <FormOutputs />
+    </FormContent>
+  </>
+);

+ 56 - 0
apps/demo-free-layout/src/nodes/http/index.tsx

@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { nanoid } from 'nanoid';
+
+import { WorkflowNodeType } from '../constants';
+import { FlowNodeRegistry } from '../../typings';
+import iconHTTP from '../../assets/icon-http.svg';
+import { FormRender } from './form-render';
+import { defaultFormMeta } from '../default-form-meta';
+let index = 0;
+
+export const HTTPNodeRegistry: FlowNodeRegistry = {
+  type: WorkflowNodeType.HTTP,
+  info: {
+    icon: iconHTTP,
+    description: 'Call the HTTP API',
+  },
+  meta: {
+    size: {
+      width: 360,
+      height: 390,
+    },
+  },
+  onAdd() {
+    return {
+      id: `http_${nanoid(5)}`,
+      type: 'http',
+      data: {
+        title: `HTTP_${++index}`,
+        api: {
+          method: 'GET',
+        },
+        body: {
+          bodyType: 'JSON',
+        },
+        headers: {},
+        params: {},
+        outputs: {
+          type: 'object',
+          properties: {
+            body: { type: 'string' },
+            headers: { type: 'object' },
+            statusCode: { type: 'integer' },
+          },
+        },
+      },
+    };
+  },
+  formMeta: {
+    render: (props) => <FormRender {...props} />,
+    effect: defaultFormMeta.effect,
+  },
+};

+ 36 - 0
apps/demo-free-layout/src/nodes/http/types.tsx

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { IFlowConstantRefValue } from '@flowgram.ai/runtime-interface';
+import { FlowNodeJSON } from '@flowgram.ai/free-layout-editor';
+import { IFlowTemplateValue, IJsonSchema } from '@flowgram.ai/form-materials';
+
+export interface HTTPNodeJSON extends FlowNodeJSON {
+  data: {
+    title: string;
+    outputs: IJsonSchema<'object'>;
+    api: {
+      method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
+      url: IFlowTemplateValue;
+    };
+    headers: IJsonSchema<'object'>;
+    headersValues: Record<string, IFlowConstantRefValue>;
+    params: IJsonSchema<'object'>;
+    paramsValues: Record<string, IFlowConstantRefValue>;
+    body: {
+      bodyType: 'none' | 'form-data' | 'x-www-form-urlencoded' | 'raw-text' | 'JSON';
+      json?: IFlowTemplateValue;
+      formData?: IJsonSchema<'object'>;
+      formDataValues?: Record<string, IFlowConstantRefValue>;
+      rawText?: IFlowTemplateValue;
+      xWwwFormUrlencoded?: IJsonSchema<'object'>;
+      xWwwFormUrlencodedValues?: Record<string, IFlowConstantRefValue>;
+    };
+    timeout: {
+      retryTimes: number;
+      timeout: number;
+    };
+  };
+}

+ 2 - 0
apps/demo-free-layout/src/nodes/index.ts

@@ -7,6 +7,7 @@ import { FlowNodeRegistry } from '../typings';
 import { StartNodeRegistry } from './start';
 import { LoopNodeRegistry } from './loop';
 import { LLMNodeRegistry } from './llm';
+import { HTTPNodeRegistry } from './http';
 import { EndNodeRegistry } from './end';
 import { ConditionNodeRegistry } from './condition';
 import { CommentNodeRegistry } from './comment';
@@ -23,4 +24,5 @@ export const nodeRegistries: FlowNodeRegistry[] = [
   CommentNodeRegistry,
   BlockStartNodeRegistry,
   BlockEndNodeRegistry,
+  HTTPNodeRegistry,
 ];

+ 2 - 1
packages/materials/form-materials/src/components/prompt-editor/index.tsx

@@ -26,6 +26,7 @@ export function PromptEditor(props: PropsType) {
     style,
     hasError,
     children,
+    disableMarkdownHighlight,
   } = props || {};
 
   const editorRef = useRef<EditorAPI | null>(null);
@@ -58,7 +59,7 @@ export function PromptEditor(props: PropsType) {
         {activeLinePlaceholder && (
           <ActiveLinePlaceholder>{activeLinePlaceholder}</ActiveLinePlaceholder>
         )}
-        <MarkdownHighlight />
+        {!disableMarkdownHighlight && <MarkdownHighlight />}
         <LanguageSupport />
         <JinjaHighlight />
         {children}

+ 1 - 0
packages/materials/form-materials/src/components/prompt-editor/types.tsx

@@ -14,5 +14,6 @@ export type PropsType = React.PropsWithChildren<{
   hasError?: boolean;
   placeholder?: string;
   activeLinePlaceholder?: string;
+  disableMarkdownHighlight?: boolean;
   style?: React.CSSProperties;
 }>;