wwf
22 小时以前 a430284aa21e3ae1f0d5654e55b2ad2852519cc2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BodyType, type HttpNodeType, Method } from '../types'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import { useNodesInteractions } from '@/app/components/workflow/hooks'
 
type Props = {
  nodeId: string
  isShow: boolean
  onHide: () => void
  handleCurlImport: (node: HttpNodeType) => void
}
 
const parseCurl = (curlCommand: string): { node: HttpNodeType | null; error: string | null } => {
  if (!curlCommand.trim().toLowerCase().startsWith('curl'))
    return { node: null, error: 'Invalid cURL command. Command must start with "curl".' }
 
  const node: Partial<HttpNodeType> = {
    title: 'HTTP Request',
    desc: 'Imported from cURL',
    method: Method.get,
    url: '',
    headers: '',
    params: '',
    body: { type: BodyType.none, data: '' },
  }
  const args = curlCommand.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []
 
  for (let i = 1; i < args.length; i++) {
    const arg = args[i].replace(/^['"]|['"]$/g, '')
    switch (arg) {
      case '-X':
      case '--request':
        if (i + 1 >= args.length)
          return { node: null, error: 'Missing HTTP method after -X or --request.' }
        node.method = (args[++i].replace(/^['"]|['"]$/g, '') as Method) || Method.get
        break
      case '-H':
      case '--header':
        if (i + 1 >= args.length)
          return { node: null, error: 'Missing header value after -H or --header.' }
        node.headers += (node.headers ? '\n' : '') + args[++i].replace(/^['"]|['"]$/g, '')
        break
      case '-d':
      case '--data':
      case '--data-raw':
      case '--data-binary':
        if (i + 1 >= args.length)
          return { node: null, error: 'Missing data value after -d, --data, --data-raw, or --data-binary.' }
        node.body = { type: BodyType.rawText, data: args[++i].replace(/^['"]|['"]$/g, '') }
        break
      case '-F':
      case '--form': {
        if (i + 1 >= args.length)
          return { node: null, error: 'Missing form data after -F or --form.' }
        if (node.body?.type !== BodyType.formData)
          node.body = { type: BodyType.formData, data: '' }
        const formData = args[++i].replace(/^['"]|['"]$/g, '')
        const [key, ...valueParts] = formData.split('=')
        if (!key)
          return { node: null, error: 'Invalid form data format.' }
        let value = valueParts.join('=')
 
        // To support command like `curl -F "file=@/path/to/file;type=application/zip"`
        // the `;type=application/zip` should translate to `Content-Type: application/zip`
        const typeMatch = value.match(/^(.+?);type=(.+)$/)
        if (typeMatch) {
          const [, actualValue, mimeType] = typeMatch
          value = actualValue
          node.headers += `${node.headers ? '\n' : ''}Content-Type: ${mimeType}`
        }
 
        node.body.data += `${node.body.data ? '\n' : ''}${key}:${value}`
        break
      }
      case '--json':
        if (i + 1 >= args.length)
          return { node: null, error: 'Missing JSON data after --json.' }
        node.body = { type: BodyType.json, data: args[++i].replace(/^['"]|['"]$/g, '') }
        break
      default:
        if (arg.startsWith('http') && !node.url)
          node.url = arg
        break
    }
  }
 
  if (!node.url)
    return { node: null, error: 'Missing URL or url not start with http.' }
 
  // Extract query params from URL
  const urlParts = node.url?.split('?') || []
  if (urlParts.length > 1) {
    node.url = urlParts[0]
    node.params = urlParts[1].replace(/&/g, '\n').replace(/=/g, ': ')
  }
 
  return { node: node as HttpNodeType, error: null }
}
 
const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {
  const [inputString, setInputString] = useState('')
  const { handleNodeSelect } = useNodesInteractions()
  const { t } = useTranslation()
 
  const handleSave = useCallback(() => {
    const { node, error } = parseCurl(inputString)
    if (error) {
      Toast.notify({
        type: 'error',
        message: error,
      })
      return
    }
    if (!node)
      return
 
    onHide()
    handleCurlImport(node)
    // Close the panel then open it again to make the panel re-render
    handleNodeSelect(nodeId, true)
    setTimeout(() => {
      handleNodeSelect(nodeId)
    }, 0)
  }, [onHide, nodeId, inputString, handleNodeSelect, handleCurlImport])
 
  return (
    <Modal
      title={t('workflow.nodes.http.curl.title')}
      isShow={isShow}
      onClose={onHide}
      className='!w-[400px] !max-w-[400px] !p-4'
    >
      <div>
        <textarea
          value={inputString}
          className='w-full my-3 p-3 text-sm text-gray-900 border-0 rounded-lg grow bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200 h-40'
          onChange={e => setInputString(e.target.value)}
          placeholder={t('workflow.nodes.http.curl.placeholder')!}
        />
      </div>
      <div className='mt-4 flex justify-end space-x-2'>
        <Button className='!w-[95px]' onClick={onHide} >{t('common.operation.cancel')}</Button>
        <Button className='!w-[95px]' variant='primary' onClick={handleSave} > {t('common.operation.save')}</Button>
      </div>
    </Modal>
  )
}
 
export default React.memo(CurlPanel)