Skip to content

实现 cURL 转代码 工具

分类:developer | 标签:curl、fetch、代码生成 cURL 命令转 fetch/axios/Python/Go/Java 请求代码

2.1 功能说明

cURL 命令转 fetch/axios/Python/Go/Java 请求代码

2.1.1 使用指南

功能说明

把一条 curl 命令解析成等价的请求代码,目前支持 JavaScript(fetch)、JavaScript(axios)、Python(requests)、Go(net/http)、Java(HttpURLConnection) 五种写法。解析器为本工具自研,不依赖第三方库,命令内容不会上传到任何服务器。

支持解析的参数

请求方法-X / --request;未显式指定时,有请求体推断为 POST,否则为 GET,-I 视为 HEAD,-G 会把数据拼到查询字符串并使用 GET。

URL--url 指定的值,或 curl 之后第一个不是选项的参数;缺少协议时自动补 https://

请求头-H / --header,形如 'Key: Value',可重复多次。

请求体-d--data--data-raw--data-binary--data-urlencode,多个数据参数会按 curl 的规则用 & 连接;工具会自动判断内容是 JSON、表单键值对还是普通文本,并据此选择目标语言里最合适的写法。

表单上传-F / --form,支持 key=valuekey=@filename 两种形式,生成 multipart/form-data 的对应代码。

其它-u user:pass 转换为 Basic 认证头,-b 转为 Cookie 头,-A 转为 User-Agent 头,-e 转为 Referer 头;-s-k-L--compressed 等只影响 curl 自身行为的开关会被忽略。

使用方式

大多数浏览器的开发者工具中,在网络面板右键某个请求选择「复制为 cURL」,直接粘贴到左侧输入框即可。支持带反斜杠换行的多行命令。粘贴后点击「转换」,在右侧切换语言标签查看代码,并可复制或下载为源文件。

注意事项

当命令带请求体但没有写 Content-Type 时,curl 实际会发送 application/x-www-form-urlencoded;如果工具检测到请求体是合法 JSON,会按更符合实际意图的方式补上 application/json 并给出提示,如不需要请在命令里显式指定请求头。

生成的代码以可读为优先,包含基本的错误处理和结果打印,实际使用时请根据项目规范调整(例如超时设置、连接复用、证书校验等)。-k 这类跳过证书校验的行为出于安全考虑不会翻译到代码中。

2.2 代码实现

2.2.1 组件结构

本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-inputel-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。

2.2.2 核心逻辑概览

<script setup> 中定义的主要函数/方法:

  • tokenize()
  • safeDecode()
  • parseCurl()
  • nextValue()
  • take()
  • q()
  • indentLines()
  • jsLiteral()
  • pyLiteral()
  • headersJs()
  • headersPy()
  • genFetch()
  • genAxios()
  • genPython()
  • genGo()
  • genJava()
  • convert()
  • copyCode()
  • downloadCode()
  • loadSample()
  • clearAll()

2.2.3 关键实现代码

以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。

vue
import { ref, computed } from 'vue'
import ToolPageShell from '@/components/ToolPageShell.vue'

const langs = [
  { key: 'fetch', label: 'JavaScript (fetch)', file: 'request.js' },
  { key: 'axios', label: 'JavaScript (axios)', file: 'request-axios.js' },
  { key: 'python', label: 'Python (requests)', file: 'request.py' },
  { key: 'go', label: 'Go (net/http)', file: 'main.go' },
  { key: 'java', label: 'Java (HttpURLConnection)', file: 'Main.java' }
]

const rawCurl = ref('')
const error = ref('')
const notice = ref('')
const lang = ref('fetch')
const parsed = ref(null)
const codes = ref({})

const bodyKindLabel = computed(() => {
  if (!parsed.value) return ''
  const map = { none: '无', json: 'JSON', form: '表单键值对', raw: '纯文本', multipart: 'multipart 表单' }
  return map[parsed.value.bodyKind] || '无'
})

/* ---------- 词法分析:按 shell 规则切分 curl 命令 ---------- */
function tokenize(source) {
  const s = String(source)
    .replace(/\\\r?\n/g, ' ')
    .replace(/\^\r?\n/g, ' ')
    .replace(/\r?\n/g, ' ')
  const tokens = []
  let cur = ''
  let started = false
  let quote = null
  for (let i = 0; i < s.length; i++) {
    const ch = s[i]
    if (quote) {
      if (ch === quote) { quote = null; continue }
      if (quote === '"' && ch === '\\') {
        const next = s[i + 1]
        if (next === '"' || next === '\\' || next === '$' || next === '`') { cur += next; i++; continue }
        if (next === 'n') { cur += '\n'; i++; continue }
        if (next === 't') { cur += '\t'; i++; continue }
        if (next === 'r') { cur += '\r'; i++; continue }
      }
      cur += ch
      continue
    }
    if (ch === '"' || ch === "'") { quote = ch; started = true; continue }
    if (ch === '\\') {
      const next = s[i + 1]
      if (next !== undefined) { cur += next; i++; started = true }
      continue
    }
    if (/\s/.test(ch)) {
      if (started) { tokens.push(cur); cur = ''; started = false }
      continue
    }
    cur += ch
    started = true
  }
  if (started) tokens.push(cur)
  return tokens
}

function safeDecode(s) {
  try { return decodeURIComponent(s) } catch (e) { return s }
}

const NO_ARG_FLAGS = new Set([
  '-s', '--silent', '-S', '--show-error', '-k', '--insecure', '-L', '--location',
  '-v', '--verbose', '-i', '--include', '--compressed', '-g', '--globoff',
  '-N', '--no-buffer', '-f', '--fail', '--http1.0', '--http1.1', '--http2',
  '-4', '-6', '-j', '--no-keepalive', '--location-trusted', '--path-as-is'
])
const VALUE_FLAGS = new Set([
  '-o', '--output', '--connect-timeout', '-m', '--max-time', '--retry',
  '-x', '--proxy', '--cert', '--key', '-w', '--write-out', '--cacert',
  '--max-redirs', '-T', '--upload-file', '--resolve', '--interface'
])

function parseCurl(source) {
  const tokens = tokenize(source)
  if (!tokens.length) throw new Error('命令为空')
  let i = 0
  if (tokens[i] === 'curl') i++
  else if (tokens[i] && tokens[i].toLowerCase().endsWith('curl')) i++

  let method = ''
  let url = ''
  const headers = []
  const dataParts = []
  const formParts = []
  let user = ''
  let isGet = false
  let headOnly = false

  const nextValue = (name) => {
    i++
    if (i >= tokens.length) throw new Error('选项 ' + name + ' 缺少参数值')
    return tokens[i]
  }

  for (; i < tokens.length; i++) {
    let t = tokens[i]
    if (t === '') continue

    // --name=value 形式
    let inlineValue = null
    if (t.startsWith('--') && t.indexOf('=') > 0) {
      const idx = t.indexOf('=')
      inlineValue = t.slice(idx + 1)
      t = t.slice(0, idx)
    }
    const take = (name) => (inlineValue !== null ? inlineValue : nextValue(name))

    if (t === '-X' || t === '--request') { method = take(t).toUpperCase(); continue }
    if (/^-X./.test(t)) { method = t.slice(2).toUpperCase(); continue }
    if (t === '-H' || t === '--header') { headers.push(take(t)); continue }
    if (/^-H./.test(t)) { headers.push(t.slice(2)); continue }
    if (t === '--url') { url = take(t); continue }
    if (t === '-d' || t === '--data' || t === '--data-raw' || t === '--data-ascii' || t === '--data-binary') {
      dataParts.push(take(t)); continue
    }
    if (t === '--data-urlencode') {
      const v = take(t)
      const eq = v.indexOf('=')
      if (eq > 0) dataParts.push(v.slice(0, eq) + '=' + encodeURIComponent(v.slice(eq + 1)))
      else dataParts.push(encodeURIComponent(v))
      continue
    }
    if (/^-d./.test(t)) { dataParts.push(t.slice(2)); continue }
    if (t === '-F' || t === '--form' || t === '--form-string') { formParts.push(take(t)); continue }
    if (t === '-u' || t === '--user') { user = take(t); continue }
    if (t === '-b' || t === '--cookie') { headers.push('Cookie: ' + take(t)); continue }
    if (t === '-A' || t === '--user-agent') { headers.push('User-Agent: ' + take(t)); continue }
    if (t === '-e' || t === '--referer') { headers.push('Referer: ' + take(t)); continue }
    if (t === '-G' || t === '--get') { isGet = true; continue }
    if (t === '-I' || t === '--head') { headOnly = true; continue }
    if (NO_ARG_FLAGS.has(t)) continue
    if (VALUE_FLAGS.has(t)) { if (inlineValue === null) nextValue(t); continue }
    // 合并式短选项,如 -fsSL
    if (/^-[a-zA-Z]{2,}$/.test(t)) {
      const chars = t.slice(1)
      let allKnown = true
      for (const c of chars) if (!NO_ARG_FLAGS.has('-' + c)) allKnown = false
      if (allKnown) continue
    }
    if (t.startsWith('-')) continue
    if (!url) url = t
  }

  if (!url) throw new Error('未识别到请求 URL')
  if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(url)) url = 'https://' + url

  let body = dataParts.join('&')
  if (isGet && body) {
    url += (url.indexOf('?') === -1 ? '?' : '&') + body
    body = ''
  }

  const headerList = []
  for (const h of headers) {
    const idx = h.indexOf(':')
    if (idx === -1) continue
    const key = h.slice(0, idx).trim()
    const value = h.slice(idx + 1).trim()
    if (key) headerList.push({ key, value })
  }
  if (user) {
    let encoded = ''
    try { encoded = btoa(unescape(encodeURIComponent(user))) } catch (e) { encoded = '' }
    if (encoded) headerList.push({ key: 'Authorization', value: 'Basic ' + encoded })
  }

  const forms = formParts.map(f => {
    const idx = f.indexOf('=')
    if (idx === -1) return { key: f, value: '', isFile: false }
    const key = f.slice(0, idx)
    const val = f.slice(idx + 1)
    if (val.startsWith('@') || val.startsWith('<')) return { key, value: val.slice(1), isFile: true }
    return { key, value: val, isFile: false }
  })

  // 判定请求体类型
  let bodyKind = 'none'
  let jsonValue = null
  let formPairs = null
  if (forms.length) {
    bodyKind = 'multipart'
  } else if (body) {
    const trimmed = body.trim()
    const looksJson = (trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))
    if (looksJson) {
      try {
        jsonValue = JSON.parse(trimmed)
        bodyKind = 'json'
      } catch (e) {
        bodyKind = 'raw'
      }
    } else if (/^[^\s=&]+=[^&]*(&[^\s=&]+=[^&]*)*$/.test(trimmed)) {
      bodyKind = 'form'
      formPairs = trimmed.split('&').map(pair => {
        const eq = pair.indexOf('=')
        return {
          key: safeDecode(pair.slice(0, eq)),
          value: safeDecode(pair.slice(eq + 1).replace(/\+/g, ' '))
        }
      })
    } else {
      bodyKind = 'raw'
    }
  }

  if (!method) {
    if (headOnly) method = 'HEAD'
    else if (bodyKind !== 'none') method = 'POST'
    else method = 'GET'
  }

  const hasContentType = headerList.some(h => h.key.toLowerCase() === 'content-type')
  let inferredContentType = ''
  if (!hasContentType) {
    if (bodyKind === 'json') inferredContentType = 'application/json'
    else if (bodyKind === 'form') inferredContentType = 'application/x-www-form-urlencoded'
    else if (bodyKind === 'raw') inferredContentType = 'text/plain'
  }
  if (inferredContentType) headerList.push({ key: 'Content-Type', value: inferredContentType })

  return { method, url, headers: headerList, body, bodyKind, jsonValue, formPairs, forms, inferredContentType }
}

/* ---------- 各语言字面量工具 ---------- */
function q(s) { return JSON.stringify(String(s)) }

function indentLines(text, spaces) {
  const pad = ' '.repeat(spaces)
  return String(text).split('\n').map((l, i) => (i === 0 ? l : pad + l)).join('\n')
}

function jsLiteral(value, level) {
  const ind = '  '.repeat(level)
  const indIn = '  '.repeat(level + 1)
  if (value === null) return 'null'
  if (typeof value === 'boolean' || typeof value === 'number') return String(value)
  if (typeof value === 'string') return q(value)
  if (Array.isArray(value)) {
    if (!value.length) return '[]'
    return '[\n' + value.map(v => indIn + jsLiteral(v, level + 1)).join(',\n') + '\n' + ind + ']'
  }
  const keys = Object.keys(value)
  if (!keys.length) return '{}'
  return '{\n' + keys.map(k => indIn + q(k) + ': ' + jsLiteral(value[k], level + 1)).join(',\n') + '\n' + ind + '}'
}

function pyLiteral(value, level) {
  const ind = '    '.repeat(level)
  const indIn = '    '.repeat(level + 1)
  if (value === null) return 'None'
  if (typeof value === 'boolean') return value ? 'True' : 'False'
  if (typeof value === 'number') return String(value)
  if (typeof value === 'string') return q(value)
  if (Array.isArray(value)) {
    if (!value.length) return '[]'
    return '[\n' + value.map(v => indIn + pyLiteral(v, level + 1)).join(',\n') + '\n' + ind + ']'
  }
  const keys = Object.keys(value)
  if (!keys.length) return '{}'
  return '{\n' + keys.map(k => indIn + q(k) + ': ' + pyLiteral(value[k], level + 1)).join(',\n') + '\n' + ind + '}'
}

function headersJs(list, level) {
  if (!list.length) return '{}'
  const ind = '  '.repeat(level)
  const indIn = '  '.repeat(level + 1)
  return '{\n' + list.map(h => indIn + q(h.key) + ': ' + q(h.value)).join(',\n') + '\n' + ind + '}'
}

function headersPy(list) {
  if (!list.length) return '{}'
  return '{\n' + list.map(h => '    ' + q(h.key) + ': ' + q(h.value)).join(',\n') + '\n}'
}

/* ---------- 代码生成 ---------- */
function genFetch(p) {
  const lines = []
  lines.push('const url = ' + q(p.url) + ';')
  lines.push('')
  if (p.bodyKind === 'json') {
    lines.push('const payload = ' + jsLiteral(p.jsonValue, 0) + ';')
    lines.push('')
  } else if (p.bodyKind === 'form') {
    lines.push('const payload = new URLSearchParams();')
    p.formPairs.forEach(pair => lines.push('payload.append(' + q(pair.key) + ', ' + q(pair.value) + ');'))
    lines.push('')
  } else if (p.bodyKind === 'multipart') {
    lines.push('const payload = new FormData();')
    p.forms.forEach(f => {
      if (f.isFile) lines.push('// ' + f.key + ' 对应文件 ' + f.value + ',浏览器中请传入 File 对象')
      lines.push('payload.append(' + q(f.key) + ', ' + (f.isFile ? '/* File 对象 */ fileInput.files[0]' : q(f.value)) + ');')
    })
    lines.push('')
  } else if (p.bodyKind === 'raw') {
    lines.push('const payload = ' + q(p.body) + ';')
    lines.push('')
  }

  const headerList = p.bodyKind === 'multipart'
    ? p.headers.filter(h => h.key.toLowerCase() !== 'content-type')
    : p.headers
  lines.push('const options = {')
  lines.push('  method: ' + q(p.method) + ',')
  lines.push('  headers: ' + indentLines(headersJs(headerList, 1), 2) + (p.bodyKind === 'none' ? '' : ','))
  if (p.bodyKind === 'json') lines.push('  body: JSON.stringify(payload)')
  else if (p.bodyKind === 'form') lines.push('  body: payload.toString()')
  else if (p.bodyKind === 'multipart' || p.bodyKind === 'raw') lines.push('  body: payload')
  lines.push('};')
  lines.push('')
  lines.push('fetch(url, options)')
  lines.push('  .then((response) => {')
  lines.push('    if (!response.ok) throw new Error("HTTP " + response.status);')
  lines.push('    return response.json();')
  lines.push('  })')
  lines.push('  .then((data) => console.log(data))')
  lines.push('  .catch((err) => console.error(err));')
  return lines.join('\n')
}

function genAxios(p) {
  const lines = []
  lines.push('import axios from "axios";')
  lines.push('')
  if (p.bodyKind === 'json') {
    lines.push('const data = ' + jsLiteral(p.jsonValue, 0) + ';')
    lines.push('')
  } else if (p.bodyKind === 'form') {
    lines.push('const data = new URLSearchParams();')
    p.formPairs.forEach(pair => lines.push('data.append(' + q(pair.key) + ', ' + q(pair.value) + ');'))
    lines.push('')
  } else if (p.bodyKind === 'multipart') {
    lines.push('const data = new FormData();')
    p.forms.forEach(f => {
      if (f.isFile) lines.push('// ' + f.key + ' 对应文件 ' + f.value + ',Node 环境可用 fs.createReadStream 传入')
      lines.push('data.append(' + q(f.key) + ', ' + (f.isFile ? '/* File 对象 */ fileInput.files[0]' : q(f.value)) + ');')
    })
    lines.push('')
  } else if (p.bodyKind === 'raw') {
    lines.push('const data = ' + q(p.body) + ';')
    lines.push('')
  }

  const headerList = p.bodyKind === 'multipart'
    ? p.headers.filter(h => h.key.toLowerCase() !== 'content-type')
    : p.headers
  lines.push('const config = {')
  lines.push('  method: ' + q(p.method.toLowerCase()) + ',')
  lines.push('  url: ' + q(p.url) + ',')
  lines.push('  headers: ' + indentLines(headersJs(headerList, 1), 2) + (p.bodyKind === 'none' ? '' : ','))
  if (p.bodyKind !== 'none') lines.push('  data: data')
  lines.push('};')
  lines.push('')
  lines.push('axios(config)')
  lines.push('  .then((response) => console.log(response.data))')
  lines.push('  .catch((error) => console.error(error.message));')
  return lines.join('\n')
}

function genPython(p) {
  const lines = []
  lines.push('import requests')
  lines.push('')
  lines.push('url = ' + q(p.url))
  lines.push('headers = ' + headersPy(p.bodyKind === 'multipart' ? p.headers.filter(h => h.key.toLowerCase() !== 'content-type') : p.headers))
  const kwargs = ['url', 'headers=headers']
  if (p.bodyKind === 'json') {
    lines.push('payload = ' + pyLiteral(p.jsonValue, 0))
    kwargs.push('json=payload')
  } else if (p.bodyKind === 'form') {
    const obj = {}
    p.formPairs.forEach(pair => { obj[pair.key] = pair.value })
    lines.push('payload = ' + pyLiteral(obj, 0))
    kwargs.push('data=payload')
  } else if (p.bodyKind === 'multipart') {
    const fields = []
    p.forms.forEach(f => {
      if (f.isFile) fields.push('    ' + q(f.key) + ': open(' + q(f.value) + ', "rb")')
      else fields.push('    ' + q(f.key) + ': (None, ' + q(f.value) + ')')
    })
    lines.push('files = {\n' + fields.join(',\n') + '\n}')
    kwargs.push('files=files')
  } else if (p.bodyKind === 'raw') {
    lines.push('payload = ' + q(p.body))
    kwargs.push('data=payload.encode("utf-8")')
  }
  lines.push('')
  const fn = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].indexOf(p.method) >= 0
    ? 'requests.' + p.method.toLowerCase()
    : 'requests.request'
  if (fn === 'requests.request') {
    lines.push('response = requests.request(' + q(p.method) + ', ' + kwargs.join(', ') + ', timeout=30)')
  } else {
    lines.push('response = ' + fn + '(' + kwargs.join(', ') + ', timeout=30)')
  }
  lines.push('')
  lines.push('print(response.status_code)')
  lines.push('print(response.text)')
  return lines.join('\n')
}

function genGo(p) {
  const tick = String.fromCharCode(96)
  const imports = ['\t"fmt"', '\t"io"', '\t"net/http"']
  const needsReader = p.bodyKind === 'json' || p.bodyKind === 'form' || p.bodyKind === 'raw'
  const isMultipart = p.bodyKind === 'multipart'
  if (needsReader) imports.push('\t"strings"')
  if (isMultipart) { imports.push('\t"bytes"'); imports.push('\t"mime/multipart"'); imports.push('\t"os"') }
  imports.sort()

  const lines = []
  lines.push('package main')
  lines.push('')
  lines.push('import (')
  imports.forEach(im => lines.push(im))
  lines.push(')')
  lines.push('')
  lines.push('func main() {')
  lines.push('\turl := ' + q(p.url))
  if (needsReader) {
    const safe = p.body.indexOf(tick) === -1
    if (safe) lines.push('\tpayload := strings.NewReader(' + tick + p.body + tick + ')')
    else lines.push('\tpayload := strings.NewReader(' + q(p.body) + ')')
  } else if (isMultipart) {
    lines.push('')
    lines.push('\tbody := &bytes.Buffer{}')
    lines.push('\twriter := multipart.NewWriter(body)')
    p.forms.forEach(f => {
      if (f.isFile) {
        lines.push('\tfile, err := os.Open(' + q(f.value) + ')')
        lines.push('\tif err != nil {')
        lines.push('\t\tfmt.Println(err)')
        lines.push('\t\treturn')
        lines.push('\t}')
        lines.push('\tdefer file.Close()')
        lines.push('\tpart, err := writer.CreateFormFile(' + q(f.key) + ', ' + q(f.value) + ')')
        lines.push('\tif err != nil {')
        lines.push('\t\tfmt.Println(err)')
        lines.push('\t\treturn')
        lines.push('\t}')
        lines.push('\tio.Copy(part, file)')
      } else {
        lines.push('\twriter.WriteField(' + q(f.key) + ', ' + q(f.value) + ')')
      }
    })
    lines.push('\twriter.Close()')
  }
  lines.push('')
  const bodyArg = needsReader ? 'payload' : (isMultipart ? 'body' : 'nil')
  lines.push('\treq, err := http.NewRequest(' + q(p.method) + ', url, ' + bodyArg + ')')
  lines.push('\tif err != nil {')
  lines.push('\t\tfmt.Println(err)')
  lines.push('\t\treturn')
  lines.push('\t}')
  const headerList = isMultipart ? p.headers.filter(h => h.key.toLowerCase() !== 'content-type') : p.headers
  headerList.forEach(h => lines.push('\treq.Header.Set(' + q(h.key) + ', ' + q(h.value) + ')'))
  if (isMultipart) lines.push('\treq.Header.Set("Content-Type", writer.FormDataContentType())')
  lines.push('')
  lines.push('\tres, err := http.DefaultClient.Do(req)')
  lines.push('\tif err != nil {')
  lines.push('\t\tfmt.Println(err)')
  lines.push('\t\treturn')
  lines.push('\t}')
  lines.push('\tdefer res.Body.Close()')
  lines.push('')
  lines.push('\tdata, err := io.ReadAll(res.Body)')
  lines.push('\tif err != nil {')
  lines.push('\t\tfmt.Println(err)')
  lines.push('\t\treturn')
  lines.push('\t}')
  lines.push('\tfmt.Println(res.Status)')
  lines.push('\tfmt.Println(string(data))')
  lines.push('}')
  return lines.join('\n')
}

function genJava(p) {
  const hasBody = p.bodyKind !== 'none'
  const isMultipart = p.bodyKind === 'multipart'
  const lines = []
  lines.push('import java.io.BufferedReader;')
  lines.push('import java.io.InputStreamReader;')
  lines.push('import java.io.OutputStream;')
  lines.push('import java.net.HttpURLConnection;')
  lines.push('import java.net.URL;')
  lines.push('import java.nio.charset.StandardCharsets;')
  if (isMultipart) lines.push('import java.nio.file.Files;')
  if (isMultipart) lines.push('import java.nio.file.Paths;')
  lines.push('')
  lines.push('public class Main {')
  lines.push('    public static void main(String[] args) throws Exception {')
  lines.push('        URL url = new URL(' + q(p.url) + ');')
  lines.push('        HttpURLConnection conn = (HttpURLConnection) url.openConnection();')
  lines.push('        conn.setRequestMethod(' + q(p.method) + ');')
  lines.push('        conn.setConnectTimeout(15000);')
  lines.push('        conn.setReadTimeout(30000);')
  const headerList = isMultipart ? p.headers.filter(h => h.key.toLowerCase() !== 'content-type') : p.headers
  headerList.forEach(h => lines.push('        conn.setRequestProperty(' + q(h.key) + ', ' + q(h.value) + ');'))

  if (isMultipart) {
    lines.push('')
    lines.push('        String boundary = "----JavaFormBoundary" + System.currentTimeMillis();')
    lines.push('        conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);')
    lines.push('        conn.setDoOutput(true);')
    lines.push('        try (OutputStream os = conn.getOutputStream()) {')
    p.forms.forEach(f => {
      if (f.isFile) {
        lines.push('            os.write(("--" + boundary + "\\r\\n").getBytes(StandardCharsets.UTF_8));')
        lines.push('            os.write(("Content-Disposition: form-data; name=' + f.key + '; filename=' + f.value + '\\r\\n").getBytes(StandardCharsets.UTF_8));')
        lines.push('            os.write("Content-Type: application/octet-stream\\r\\n\\r\\n".getBytes(StandardCharsets.UTF_8));')
        lines.push('            os.write(Files.readAllBytes(Paths.get(' + q(f.value) + ')));')
        lines.push('            os.write("\\r\\n".getBytes(StandardCharsets.UTF_8));')
      } else {
        lines.push('            os.write(("--" + boundary + "\\r\\n").getBytes(StandardCharsets.UTF_8));')
        lines.push('            os.write(("Content-Disposition: form-data; name=' + f.key + '\\r\\n\\r\\n").getBytes(StandardCharsets.UTF_8));')
        lines.push('            os.write(' + q(f.value) + '.getBytes(StandardCharsets.UTF_8));')
        lines.push('            os.write("\\r\\n".getBytes(StandardCharsets.UTF_8));')
      }
    })
    lines.push('            os.write(("--" + boundary + "--\\r\\n").getBytes(StandardCharsets.UTF_8));')
    lines.push('        }')
  } else if (hasBody) {
    lines.push('')
    lines.push('        String body = ' + q(p.body) + ';')
    lines.push('        conn.setDoOutput(true);')
    lines.push('        try (OutputStream os = conn.getOutputStream()) {')
    lines.push('            os.write(body.getBytes(StandardCharsets.UTF_8));')
    lines.push('        }')
  }

  lines.push('')
  lines.push('        int status = conn.getResponseCode();')
  lines.push('        BufferedReader reader = new BufferedReader(new InputStreamReader(')
  lines.push('                status >= 400 ? conn.getErrorStream() : conn.getInputStream(), StandardCharsets.UTF_8));')
  lines.push('        StringBuilder sb = new StringBuilder();')
  lines.push('        String line;')
  lines.push('        while ((line = reader.readLine()) != null) {')
  lines.push('            sb.append(line);')
  lines.push('        }')
  lines.push('        reader.close();')
  lines.push('        conn.disconnect();')
  lines.push('')
  lines.push('        System.out.println(status);')
  lines.push('        System.out.println(sb.toString());')
  lines.push('    }')
  lines.push('}')
  return lines.join('\n')
}

function convert() {
  error.value = ''
  notice.value = ''
  parsed.value = null
  codes.value = {}
  if (!rawCurl.value.trim()) { error.value = '请输入 curl 命令'; return }
  let p
  try {
    p = parseCurl(rawCurl.value)
  } catch (e) {
    error.value = '解析失败:' + e.message
    return
  }
  parsed.value = p
  codes.value = {
    fetch: genFetch(p),
    axios: genAxios(p),
    python: genPython(p),
    go: genGo(p),
    java: genJava(p)
  }
  if (p.inferredContentType === 'application/json') {
    notice.value = '原命令未指定 Content-Type,检测到请求体是 JSON,已按 application/json 生成(curl 默认会发送 application/x-www-form-urlencoded)'
  }
}

function copyCode() {
  const code = codes.value[lang.value]
  if (code && navigator.clipboard) navigator.clipboard.writeText(code)
}

function downloadCode() {
  const code = codes.value[lang.value]
  if (!code) return
  const meta = langs.find(l => l.key === lang.value)
  const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = meta ? meta.file : 'request.txt'
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  URL.revokeObjectURL(url)
}

function loadSample() {
  rawCurl.value = [
    "curl -X POST 'https://api.example.com/v1/users' \\",
    "  -H 'Content-Type: application/json' \\",
    "  -H 'Authorization: Bearer token123' \\",
    '  -d \'{"name":"张三","age":28,"tags":["vip","new"],"active":true}\''
  ].join('\n')
  convert()
}

function clearAll() {
  rawCurl.value = ''
  parsed.value = null
  codes.value = {}
  error.value = ''
  notice.value = ''
}

2.3 效果截图

cURL 转代码 效果截图

工具访问地址:https://www.i91tools.com/tools/curl-convert