CodeCompanion.nvim 底层 HTTP 利器:Plenary.Curl 库完整解析与实战指南
【免费下载链接】codecompanion.nvim✨ AI Coding, Vim Style项目地址: https://gitcode.com/GitHub_Trending/co/codecompanion.nvim
导读
CodeCompanion.nvim 的绝大多数 HTTP 通信——从与 Anthropic、OpenAI、Ollama 等模型的请求往来,到 GitHub Copilot 的 Token 换取与用量统计,再到聊天中拉取远程图片——都建立在 Plenary.Curl 这一层薄薄的 curl 封装之上。本篇基于仓库中的 Plenary.Curl 源码(.codecompanion/adapters/plenary_curl.md),完整梳理其参数模型、返回结构、底层 curl 参数拼装逻辑与同步/异步两种调用方式,并结合 http.lua、token.lua、stats.lua、get_models.lua 等真实调用点,帮助读者彻底搞懂这条 HTTP 链路的每个环节,进而在自己的 Neovim 插件或 CodeCompanion 自定义适配器中熟练使用 Plenary.Curl。
一、Plenary.Curl 是什么
Plenary.Curl 是 plenary.nvim 中内置的 curl 包装库(作者为 github.com/tami5)。它把裸curl命令行调用封装成一组表驱动的 Lua API:开发者只需传入 Lua table 形式的参数,库内部就会把它们翻译成 curl 的 argv 数组,通过plenary.job异步执行,并把响应解析回结构化的 Lua table。
它天然带有三个特性,这也正是 CodeCompanion.nvim 选择它的原因:
- 零额外依赖:只要系统里有
curl可执行文件即可工作; - 同步/异步双模式:既能阻塞等待返回完整响应,也能以回调方式流式接收 stdout;
- 贴近 curl 语义:几乎所有 curl 的常用能力(认证、代理、表单、超时、HTTP 版本、忽略证书校验等)都能通过简单参数透传。
从源码结构看,该库由四个部分组成(对应 plenary_curl.md 中的分区):
util.*:URL 编码、KV 表转换、临时转储路径生成等工具函数;parse.*:把用户参数逐项翻译成 curl 参数数组的解析函数;parse.request/parse.response:请求参数的统一拼装与响应解析;- 模块末尾返回的
get / post / put / head / patch / delete / request七个方法入口。
二、核心 API:统一参数模型与返回值
Plenary.Curl 的设计哲学是「一个参数模型,七个方法入口」。所有 curl 方法(get、post、put、head、patch、delete、request)都接受同一套参数 table,返回同一个结构的响应 table。
2.1 请求参数表
源码开头的文档注释给出了完整参数定义:
| 参数 | 类型 | 说明 |
|---|---|---|
url | string | 要发起请求的 URL |
query | table | URL 查询参数,会自动追加到 url 之后 |
body | string / filepath / table | 请求体。table 按 form 数据编码,字符串若指向存在的文件则按文件内容发送 |
auth | string / array | Basic 认证,形如"user:pass"或{"user", "pass"} |
form | table | 表单参数,翻译为 curl 的-F |
raw | array | 任意额外的 curl 参数,必须是数组/列表形式 |
dry_run | boolean | 为true时不真正执行,直接返回要交给 curl 的 argv 数组 |
output | filepath | 下载目标路径,翻译为-o |
timeout | number | 请求超时时间(毫秒) |
http_version | string | HTTP 版本:'HTTP/0.9'、'HTTP/1.0'、'HTTP/1.1'、'HTTP/2'、'HTTP/3' |
proxy | string | 代理,格式[protocol://]host[:port] |
insecure | boolean | 是否允许不安全连接(忽略 TLS 证书校验) |
此外,从 request 函数 的实现中可以发现文档注释未列全的扩展参数:
method:显式指定 HTTP 方法;headers:请求头 KV 表;accept:Accept头内容(翻译为-H "Accept: ...");compressed:是否启用--compressed(非 Windows 平台默认为true);stream:流式回调函数,作为on_stdout使用;callback:请求完成后的回调,提供回调时进入异步模式;on_error:curl 退出码非 0 时的错误回调;dump:响应头转储文件路径(一般由内部自动生成)。
2.2 响应结构
无论同步还是异步,成功返回的响应都是如下结构的 table:
| 字段 | 类型 | 说明 |
|---|---|---|
exit | number | 底层 shell 进程的退出码 |
status | number | HTTP 响应状态码 |
headers | array | HTTP 响应头(字符串行数组) |
body | string | HTTP 响应体 |
源码中的 parse.response 展示了它如何从 curl-D转储的响应头文件中逐行提取状态码:用模式^HTTP/%S*%s+(%d+)匹配HTTP/1.1 200 OK这类行,把第一处匹配的200作为status;其余非空行收进headers;body则由plenary.functional的F.join把所有 stdout 行拼成字符串。解析完成后会立即vim.loop.fs_unlink删除临时头文件。
2.3 七个方法入口
模块末尾(plenary_curl.md#L339-L364)通过partial闭包生成了七个方法:
return { get = partial "get", post = partial "post", put = partial "put", head = partial "head", patch = partial "patch", delete = partial "delete", request = partial "request", }partial的实现有两点值得注意:
- 既支持
Curl.get(url, opts),也支持直接把url放进 opts 表、整体传参:Curl.get({ url = "...", ... }); opts = method == "request" and opts or vim.tbl_extend("keep", opts, spec):除request外的所有方法会把method字段合并进参数表;而request方法要求用户自行通过method参数指定(或使用 opts 表时自动带上)。
三、参数如何变成 curl 命令:底层拼装机制
这是 Plenary.Curl 最核心、也最值得深读的部分。所有参数都会在 parse.request 中被逐项翻译成 curl 的 argv,最终结果形如:
-sSL <dump> [--insecure] [--proxy ...] [--compressed] [-X METHOD] [-H "K: V"] ... [-d k=v] [-F k=v] [-d @file] [-u user:pass] [--http2] [raw...] [-o out] <url>3.1 body 参数的三态分派
body是灵活性最高的参数,parse.request 开头 对它做了三种分派:
if type(b) == "table" then opts.data = b -- 1. table → 作为 -d key=value 表单数据 elseif silent_is_file() then opts.in_file = b -- 2. 字符串且指向真实文件 → -d @file(文件内容) elseif type(b) == "string" then opts.raw_body = b -- 3. 普通字符串 → --data-raw 原样发送 end注意这里的silent_is_file使用pcall(P.is_file, ...)包裹,即使传入的不是文件路径也不会抛错,而是安全回退到原始字符串分支。CodeCompanion 正是利用「body 可以是指向文件的路径」这一特性,把 JSON 请求体先写入临时文件再交给 curl(见下文第四节)。
3.2 各 parse 函数的翻译规则
源码中的解析函数逐一对应着 curl 参数:
| 参数/场景 | 翻译结果 | 对应函数 |
|---|---|---|
headers表 | -H "Header-Name: value"(下划线转连字符、首字母大写) | parse.headers |
accept | -H "Accept: <值>" | parse.accept_header |
data表 | -d key=value(每个键值对一条) | parse.data_body |
raw_body字符串 | --data-raw <值> | parse.raw_body |
form表 | -F key=value | parse.form |
query表 | key1=value1&key2=value2,追加到 url 后用?连接 | parse.curl_query+parse.url |
method(非 head) | -X <大写方法名> | parse.method |
method == "head" | -I(HEAD 请求专用旗标) | parse.method |
in_file | -d @<绝对路径> | parse.file |
auth | -u user:pass或-u "user:pass" | parse.auth |
http_version | HTTP/2→--http2(校验后小写并去掉/) | parse.http_version |
insecure | --insecure | parse.request内联 |
proxy | --proxy <值> | parse.request内联 |
output | -o <路径> | parse.request内联 |
值得一提的细节:
- 基础参数固定为
-sSL(plenary_curl.md#L222),即静默模式 + 跟随重定向 + 显示错误; parse.http_version会校验取值,传入未知版本直接error "Unknown HTTP version.";parse.url对 table 类型的 url 直接报错:error "Low level URL definition is not supported.",低层 URL 定义不被支持;parse.headers做规范化:content_type→Content-Type,即把下划线换成连字符并按词首大写处理。
3.3 响应头转储与gen_dump_path
为了拿到响应头,Plenary.Curl 会让 curl 用-D把响应头写入临时文件(util.gen_dump_path,plenary_curl.md#L77-L90)。临时文件路径规则:
- Windows:
%USERPROFILE%\AppData\Local\Temp\plenary_curl_<id>.headers; - 其他平台:
$XDG_RUNTIME_DIR(未设置则/tmp)下的plenary_curl_<id>.headers。
<id>由math.random生成的十六进制串填充,避免并发请求互相覆盖。
3.4 默认值合并
正式发请求前(plenary_curl.md#L283-L289),会用vim.tbl_extend("force", {...}, specs)合并默认值:
local args, opts = parse.request(vim.tbl_extend("force", { compressed = package.config:sub(1, 1) ~= "\\", -- 非 Windows 默认启用压缩 dry_run = false, dump = util.gen_dump_path(), }, specs))即:默认启用--compressed(Windows 除外)、默认dry_run = false、自动生成响应头转储文件。dry_run = true时request直接返回 args 数组,方便调试——这一特性对排查请求问题非常有用。
四、同步与异步:两种调用范式
request 主体 基于plenary.job构建任务,执行命令取自全局变量vim.g.plenary_curl_bin_path,未设置时回退为"curl"——这意味着用户可以自定义 curl 二进制路径。
4.1 异步模式(推荐,不阻塞 Neovim)
只要传了callback或stream,就进入异步模式:
Curl.get("https://api.example.com/v1/models", { headers = { Authorization = "Bearer " .. token }, callback = function(response) -- response.status / response.headers / response.body local ok, json = pcall(vim.json.decode, response.body) end, })此时job:start()立即返回 job 对象,Neovim 事件循环不被阻塞。若传入stream,每次 stdout 输出都会回调,CodeCompanion 的 SSE 流式响应正是借助这一机制实现的。curl 退出码非 0 时:若提供了on_error则调用它,否则直接error(),错误信息包含方法、URL、退出码与 stderr 内容(plenary_curl.md#L304-L317)。
4.2 同步模式
不传回调时走同步路径:job:sync(timeout),默认超时10000毫秒(plenary_curl.md#L331),返回解析后的响应 table。适合工具脚本、模型列表拉取等一次性调用场景。
4.3 在 CodeCompanion 中的实践
仓库对两种模式都有真实用例:
- 同步:copilot/stats.lua 用
Curl.get("https://api.github.com/copilot_internal/user", { sync = true, ... })拉取 Copilot 用量统计,随后vim.json.decode(response.body)解析额度快照; - 异步回调:adapters/utils/models/fetch.lua 用
Curl.get(url, { callback = vim.schedule_wrap(...) })异步拉取模型列表,配合vim.wait实现「先异步发起、必要时阻塞等待」的混合策略; - 流式:http.lua 为流式请求设置
request_opts["stream"] = self.methods.schedule_wrap(...),逐块接收 SSE 数据并写入响应日志文件。
五、CodeCompanion.nvim 如何站在 Plenary.Curl 之上
理解 Plenary.Curl 后,再看 CodeCompanion 的 HTTP 层就一目了然了。
5.1 统一 HTTP 客户端(http.lua)
lua/codecompanion/http.lua 是核心封装,开头即local Curl = require("plenary.curl"),并通过静态方法表便于测试 mock:
Client.static.methods = { post = { default = Curl.post }, get = { default = Curl.get }, ... }它做了几件 Plenary.Curl 本身不负责的事:
- 请求体写临时文件:
write_body_file用vim.fn.tempname() .. ".json"生成临时文件,把编码后的 JSON body 写进去,再以body = body_file传给 Curl——正好命中 Plenary.Curl 的「body 是文件路径」分支; - 请求头写文件:
write_headers_file把 headers 逐行写入--header @file所需的文件; - 附加 curl 原始参数:
build_curl_args注入--retry 3 --retry-delay 1 --keepalive-time 60 --connect-timeout 10,流式时再加--tcp-nodelay --no-buffer,并通过raw参数透传给 Plenary.Curl; - 错误处理:HTTP 状态码 ≥ 400 时把响应包装为
{ message, stderr, status }错误。
5.2 GitHub Copilot 适配器
Copilot 相关的两处调用直观展示了 Plenary.Curl 的典型用法:
- copilot/token.lua:
Curl.get("https://api.github.com/copilot_internal/v2/token", { headers = { Authorization = "Bearer " .. oauth }, on_error = ... })换取 Copilot 会话 Token,并设置_token_fetch_in_progress锁避免并发重复请求; - copilot/stats.lua:携带
Authorization、Accept: */*、User-Agent三个头同步获取用量数据。
5.3 Ollama 模型列表
ollama/get_models.lua 是异步嵌套的典型:先Curl.get(url .. "/api/tags", { callback = ... })拉模型清单,在回调里再对每个模型发起Curl.post(url .. "/api/show", { body = vim.json.encode({ model = name }) }),并维护pending表与_running标志来控制并发与完成判定。这正是「基于 Plenary.Curl 的回调式编程」的生动范例。
5.4 远程图片下载
utils/images.lua 展示了output参数的实际价值:Curl.get(url, { output = loc, callback = ... })把图片直接下载到临时文件,再从响应头中解析Content-Type得到 mimetype,进而 base64 编码后发给多模态模型。
六、实用技巧与注意事项
结合源码实现,总结几条实战经验:
- 调试用
dry_run:遇到请求异常时,设dry_run = true拿到完整 argv 数组,可以直接在 shell 里复现:local args = Curl.post({ url = "https://...", body = {...}, dry_run = true }) print(vim.inspect(args)) - 响应头在
headers里是字符串行:如需结构化取值,可像 utils/images.lua 那样用line:match("^([^:]+):%s*(.+)$")自行解析键值。 body传文件路径可避免大请求体占用内存:CodeCompanion 的 JSON 请求体就采用写临时文件的方式,这也是 Plenary.Curl 三态分派设计的初衷。- 流式响应务必包一层
vim.schedule_wrap:回调中直接操作 buffer/extmark 等 Neovim API 时,应像 http.lua 那样调度回主循环,避免在 job 的线程回调中触发 API 竞态。 - 同步调用注意超时:默认 10 秒超时对模型推理类请求可能不够,务必显式传
timeout(毫秒)。CodeCompanion 在 http.lua 的 send_sync 中默认给了 120000 毫秒。 - 自定义 curl 路径:通过
vim.g.plenary_curl_bin_path可替换默认的curl二进制,适合受限环境或需要特定版本的场景。
七、小结
Plenary.Curl 的价值在于:用一张参数表统一了 curl 的近百个命令行旗标,用plenary.job提供了不阻塞 Neovim 的异步能力,再用统一的{ exit, status, headers, body }响应结构抹平了底层差异。CodeCompanion.nvim 的 HTTP 适配器体系、Copilot 令牌管理、Ollama 模型发现乃至图片拉取,全部建立在这层薄薄的封装之上。掌握了本文的参数模型、翻译规则与同步/异步范式,无论是排查 CodeCompanion 的网络问题,还是在自己基于 plenary.nvim 的插件中发起 HTTP 请求,都将事半功倍。
【免费下载链接】codecompanion.nvim✨ AI Coding, Vim Style项目地址: https://gitcode.com/GitHub_Trending/co/codecompanion.nvim
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考