如果你已经在电脑上使用了 chat.nvim 这个 Neovim AI 对话插件,那么你一定会想知道:能否在手机上继续这些对话?
答案是可以的,chat.nvim 支持链接多种 IM 工具,包括微信、飞书、Telegram、Discord 等等。
和 IM 不同的是,Nova 是通过 chat.nvim 的 REST API 来链接各个会话,是一个专门为 chat.nvim 设计的 Android 客户端。
Nova 是一个 Android 原生应用,它通过连接 chat.nvim 的 HTTP Server,让你可以在手机上继续你的 AI 对话。
简单来说:
在手机上继续你的 Neovim AI 对话,无需重新开始,历史记录完整同步。
设置信息和会话列表保存在本地,保护你的隐私。
在手机上使用 Nova 之前,你需要:
ChatApp.apk首次使用需要配置服务器信息:
配置完成后:
Nova 是一个原生 Android 应用,使用以下技术:
| 技术 | 说明 |
|---|---|
| 语言 | Java |
| 最低 SDK | Android 7.0 (API 24) |
| 目标 SDK | Android 14 (API 34) |
| UI 框架 | AppCompat + Material Design |
| 网络请求 | OkHttp 4.12.0 |
| Markdown | Markwon 4.6.2 |
| JSON 解析 | org.json |
┌────────────────────┐
│ Tools / MCP │
│ (Editor) │
└────────────────────┘
▲
│ Async Job
▼
┌──────────────┐ HTTP API ┌──────────────┐ AI API ┌────────────┐
│ Nova │ ◄──────────► │ chat.nvim │ ◄────────► │ AI Model │
│ (Android) │ │ HTTP Server │ │ │
└──────────────┘ └──────────────┘ └────────────┘
▲
│
│ user input / result
▼
┌──────────────┐
│ Neovim │
│ Floating UI │
└──────────────┘
数据流:
重要: Nova 不直接连接大模型 API,而是通过 chat.nvim HTTP Server 作为中间层。
在家里、办公室、咖啡馆,无论在哪,只要你的电脑开启了 chat.nvim HTTP Server,就能通过手机随时访问。
| 会话列表 | 聊天界面 | 设置界面 |
|---|---|---|
Nova 还在不断进化中,后续计划支持:
如果你是开发者,想要参与贡献:
Nova 是 chat.nvim 生态的重要补充,它打破了桌面环境的限制,让你可以在移动设备上继续使用 AI 辅助开发。无论你是在通勤路上、咖啡馆里,还是任何远离电脑的地方,只要有网络连接,就能随时访问你的 AI 助手。
相关项目:picker.nvim - Neovim fuzzy finder
如果你在使用过程中遇到问题,欢迎在 GitHub Issues 提交反馈!
在开发 chat.nvim 插件时,
遇到了一个奇怪的错误:当使用 curl 发送较大的 JSON 请求体时,
会报错 ENAMETOOLONG: name too long。这个错误提示虽然明确,但背后的原因和解决方案却值得记录一下。
chat.nvim 是一个 Neovim 插件,用于在 Neovim 内集成 AI 助手功能。 在调用 AI API 时,需要使用 job.nvim 异步调用 curl 发送 POST 请求, 请求体是一个 JSON 对象,包含了模型名称、对话历史、工具定义等信息。
原始的实现方式是这样的:
local cmd = {
'curl',
'-s',
url,
'-H',
'Content-Type: application/json',
'-H',
'Authorization: Bearer ' .. api_key,
'-X',
'POST',
'-d',
vim.json.encode({
model = requestObj.model,
messages = requestObj.messages,
stream = true,
stream_options = { include_usage = true },
tools = require('chat.tools').available_tools(),
}),
}
local jobid = job.start(cmd, {
on_stdout = requestObj.on_stdout,
on_stderr = requestObj.on_stderr,
on_exit = requestObj.on_exit,
})
这种方式将 JSON 数据直接作为 curl 的 -d 参数传递,看似没问题,但当对话历史较长或者工具定义较多时,JSON 数据会变得很大,导致命令行参数过长。
ENAMETOOLONG 错误表明命令行参数超过了系统限制。不同操作系统对命令行参数长度有不同的限制:
MAX_ARG_STRLEN)当 JSON 数据包含大量对话历史或工具定义时,很容易超过这些限制。比如,一个包含 50 条对话记录的请求,JSON 可能就有几十 KB,再加上 URL、Header 等参数,总长度很容易超标。
curl 提供了一个非常实用的特性:使用 -d @- 从 stdin 读取数据。这样就可以避免命令行参数长度限制,将请求体通过 stdin 传递给 curl。
修改后的代码:
local cmd = {
'curl',
'-s',
url,
'-H',
'Content-Type: application/json',
'-H',
'Authorization: Bearer ' .. api_key,
'-X',
'POST',
'-d',
'@-', -- 从 stdin 读取数据
}
local body = vim.json.encode({
model = requestObj.model,
messages = requestObj.messages,
thinking = {
type = 'enabled',
},
stream = true,
stream_options = { include_usage = true },
tools = require('chat.tools').available_tools(),
})
local jobid = job.start(cmd, {
on_stdout = requestObj.on_stdout,
on_stderr = requestObj.on_stderr,
on_exit = requestObj.on_exit,
})
-- 通过 stdin 发送请求体
job.send(jobid, body)
job.send(jobid, nil) -- 关闭 stdin,表示数据发送完毕
关键变化:
-d 参数从 JSON 字符串改为 @-,表示从 stdin 读取body 变量job.send() 通过 stdin 发送数据nil 关闭 stdin,告诉 curl 数据已发送完毕这个问题的修复记录在 commit @3f4277:
commit 3f427762779ff9ffe645fd6683195da0b234731d
Author: Eric Wong <[email protected]>
Date: Sun Feb 8 21:41:47 2026 +0800
fix: use stdin to send body
use body as curl command argument cause error:
`ENAMETOOLONG: name too long`
修复涉及了三个 provider 文件:deepseek.lua、github.lua、moonshot.lua,统一采用了 stdin 方式发送请求体。
在使用 stdin 发送数据时,有几个细节需要注意:
及时关闭 stdin:发送完数据后必须关闭 stdin,否则 curl 会一直等待更多数据,导致请求无法完成。使用 job.send(jobid, nil) 或 job.chanclose(jobid, 'stdin') 都可以实现。
数据完整性:确保发送的 JSON 数据是完整的,不要分多次发送部分数据,除非你知道如何处理分块传输。
编码问题:确保发送的数据是正确的 UTF-8 编码,curl 默认期望 UTF-8。
当使用 curl 发送大量数据时,不要将数据直接作为命令行参数传递,而应该使用 stdin(-d @-)方式。这样可以:
这个坑虽然看似简单,但在实际开发中却容易被忽视。记录下来,希望能帮助到遇到类似问题的开发者。
如果你也在开发类似的 HTTP 客户端功能,建议一开始就采用 stdin 方式,避免后续遇到这个问题。
本文记录在使用 阿里云百炼平台 Coding Plan(GLM-5 模型) 时遇到的另一个严重 bug:流式输出内容截断。
上一篇博文记录了 Tool Call arguments 输出协议异常的问题,这一次遇到了更基础的问题:普通文本回复的内容被截断,直接丢失了最后的几个字符。
阿里云售后人员的回复是:”这是工具问题”。好吧,那就用这篇博文来证明,这根本不是工具问题。
在 chat.nvim 中使用 GLM-5 模型,发送最简单的问候消息 “你好”。这次请求完全没有触发 Tool Call,就是一次最普通的聊天对话。
以下是 curl stdout 的完整原始日志,逐行打印,未做任何删减:
data: {"choices":[{"delta":{"content":null,"reasoning_content":"用户","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"说"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"\"你好"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"\","},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"这是一个简单的"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"问候。"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"根据我的"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"性格设定"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":",我应该"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":":\n1"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"."},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":" 热"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"情"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"友好\n"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"2."},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":" 简"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"洁直接"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"\n3"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"."},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":" 可能使用"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"一些表情"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"符号\n\n"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"我应该回忆"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"一下是否"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"有什么相关的"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"记忆,"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"比如用户的"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"偏好等"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"。不过"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"这是一个新的"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"对话开始"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":",我先"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"友好地"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"回应,"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"看看用户"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"需要什么"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"帮助。\n\n"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"我应该:\n"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"-"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":" 友好"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"地问候"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"\n-"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":" 简单"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"介绍自己"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"可以"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"做什么\n"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"- "},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"询问需要"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"什么帮助"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"\n\n"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"不需要调用"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"任何工具"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":",只是"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"简单的对话"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":null,"reasoning_content":"问候。"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"你好!","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"我是 No","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"va,","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"来自 N","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"eovim 的小","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"星星 :","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":")","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"\n\n","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"我可以","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"帮","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"你:\n- 📝","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":" 编","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"写和修","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"改 ","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"Lu","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"a 插","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"件代码","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"\n- 🔍","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":" 搜","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"索、","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"阅读项目文件\n","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"- 📦","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":" Gi","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"t 操","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"作(提","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"交、","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"分支","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"管","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"理等)\n","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"- 💾","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":" 记","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"住你","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"的偏好和习","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"惯","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"\n- 🌐","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":" 搜索网","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"络、获","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"取网页内","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"容\n\n","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"有什么","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"delta":{"content":"我","reasoning_content":null},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[{"finish_reason":"stop","delta":{"content":"","reasoning_content":null},"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: {"choices":[],"object":"chat.completion.chunk","usage":{"prompt_tokens":16418,"completion_tokens":191,"total_tokens":16609,"completion_tokens_details":{"reasoning_tokens":106},"prompt_tokens_details":{"cached_tokens":16414}},"created":1776597535,"system_fingerprint":null,"model":"glm-5","id":"chatcmpl-f87aa751-9260-9884-8cb6-f33c540cab94"}
data: [DONE]
从完整日志中可以清楚看到:
reasoning_content(思考过程)输出正常,从 “用户” 到 “问候。” 共 55 行content(正式回复)开始输出,从 “你好!” 到功能介绍{"content":"我"}finish_reason: "stop" 的 chunk,content 是空字符串[DONE]按照正常的回复逻辑,AI 应该说 “有什么我可以帮你的吗?”,但实际只输出了 “有什么我”,后面的 7 个字符”可以帮你的吗?”直接丢失了。
chat.nvim 的请求逻辑非常简单:
function M.request(opt)
local cmd = {
'curl',
'-s',
'https://coding.dashscope.aliyuncs.com/v1/chat/completions',
'-H', 'Content-Type: application/json',
'-H', 'Authorization: Bearer ' .. api_key,
'-X', 'POST',
'-d', '@-', -- 从 stdin 读取请求体
}
-- 启动 job,通过 stdin 发送请求体
local jobid = job.start(cmd, {
on_stdout = opt.on_stdout, -- 处理 stdout 输出
on_stderr = opt.on_stderr,
on_exit = opt.on_exit,
})
job.send(jobid, body)
end
SSE 解析逻辑也只是简单地:
data: 前缀content 字段if choice.delta.content and #choice.delta.content > 0 then
sessions.on_progress(id, choice.delta.content)
end
| 论点 | 证据 |
|---|---|
| curl 只是传输层 | curl 不做任何内容处理,只是把 stdout 原样输出 |
| 日志是完整原始输出 | 日志是逐行打印的,不存在截断或遗漏 |
| SSE 格式正常 | 每个 chunk 都是合法的 data: {...} 格式 |
| finish_reason 正确返回 | 服务端明确返回了 finish_reason: "stop" |
| usage 统计显示完成 | completion_tokens: 191 表明模型认为已完成 |
结论:服务端已经认为回复完成了(finish_reason: "stop"),但实际输出不完整。这是服务端生成逻辑的 bug,客户端无法检测或修复。
根据输出特征,推测可能的原因:
finish_reason: stop 的时机不同步有趣的是,usage 显示 completion_tokens: 191,其中 reasoning_tokens: 106。这意味着:
但实际输出的 content 远少于 85 tokens(只有几个短句),说明:
上一篇博文记录的 Tool Call bug 是 arguments 输出协议异常。这次遇到的是更基础的 content 截断问题。
两个问题的共同特征:
| 特征 | Tool Call Bug | Content 截断 Bug |
|---|---|---|
| 问题层级 | 协议层 | 内容生成层 |
| 表现形式 | 数据格式异常 | 数据丢失 |
| 影响范围 | Tool Call 功能 | 所有回复 |
| 客户端可检测 | ✅ 可以检测并容错 | ❌ 无法检测 |
Content 截断 bug 更加隐蔽,因为:
stop,客户端会认为正常完成| 影响项 | 严重程度 |
|---|---|
| 回复不完整 | 高 |
| 功能介绍缺失 | 中 |
| 用户体验下降 | 高 |
| 无法自动检测 | 高 |
用户发送消息后,收到的是不完整的回复。例如:
用户可能会:
客户端无法修复这个问题,因为:
finish_reason: "stop" 表示正常结束唯一的”修复”方式是:用户自己发现回复不完整,手动再次询问。
这是一个 服务端内容生成层 bug,具体表现为:
finish_reason: "stop"finish_reason: "stop" 之前,确保所有 content 已完整输出建议阿里云增加以下测试:
# 简单测试:检查输出是否完整
curl -N https://coding.dashscope.aliyuncs.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_KEY" \
-d '{"model":"glm-5","messages":[{"role":"user","content":"你好"}],"stream":true}' \
> output.txt
# 分析:最后一个 content chunk 后是否直接出现 finish_reason: stop
grep "content" output.txt | tail -5
grep "finish_reason" output.txt
又一次踩坑阿里云 Coding Plan。上一篇是 Tool Call 协议异常,这次是 Content 截断。
阿里云售后说”这是工具问题”,但本文已经清楚证明:
finish_reason: "stop"这是服务端 bug,不是工具问题。
建议使用阿里云 Coding Plan 的开发者:
本文记录在使用 阿里云百炼平台 Coding Plan(GLM-5 模型) 时遇到的一个流式输出协议异常问题。请求客户端为 Neovim 插件 chat.nvim,通过 curl 命令异步调用阿里云的流式聊天补全接口。
在正常开发过程中,chat.nvim 需要调用大模型完成代码生成、问题解答等任务,并支持 Tool Call 功能以扩展模型能力。然而在实际使用中,发现当模型返回 Tool Call 响应时,SSE(Server-Sent Events)流中会夹杂未经封装的原始文本数据,导致客户端解析失败。
chat.nvim 底层使用 Neovim 的 job 系统异步执行 curl 命令,通过标准输入输出与服务端通信。核心请求函数如下:
function M.request(opt)
local cmd = {
'curl',
'-s',
'https://coding.dashscope.aliyuncs.com/v1/chat/completions',
'-H',
'Content-Type: application/json',
'-H',
'Authorization: Bearer ' .. config.config.api_key.aliyuncs_pc,
'-X',
'POST',
'-d',
'@-',
}
local body = vim.json.encode({
model = sessions.get_session_model(opt.session),
messages = opt.messages,
enable_thinking = true,
stream = true,
stream_options = { include_usage = true },
tools = require('chat.tools').available_tools(),
})
local jobid = job.start(cmd, {
on_stdout = opt.on_stdout,
on_stderr = opt.on_stderr,
on_exit = opt.on_exit,
})
job.send(jobid, body)
job.send(jobid, nil)
sessions.set_session_jobid(opt.session, jobid)
return jobid
end
| 参数 | 配置值 | 说明 |
|---|---|---|
| 服务端点 | https://coding.dashscope.aliyuncs.com/v1/chat/completions |
阿里云百炼平台 API |
| 模型 | glm-5 |
GLM-5 模型(Coding Plan) |
| 流式输出 | stream = true |
启用 SSE 流式响应 |
| Tool Call | tools = available_tools() |
注册可用工具函数 |
| 思考模式 | enable_thinking = true |
启用模型思考过程 |
| 使用量统计 | stream_options.include_usage = true |
返回 token 使用统计 |
请求体通过 curl -d @- 从标准输入读取,job 系统通过 job.send() 将 JSON 编码的请求体发送给 curl 进程。响应数据通过 on_stdout 回调逐行接收,客户端需要实时解析 SSE 格式的数据流。
符合 OpenAI 规范的 SSE 流式响应应当严格遵循以下格式,每一行都以 data: 前缀开头,后面跟随完整的 JSON 对象或 [DONE] 标记:
data: {"id":"chat-123","choices":[{"delta":{"content":"Hello"},"index":0}]}
data: {"id":"chat-123","choices":[{"delta":{"content":" world"},"index":0}]}
data: {"id":"chat-123","choices":[{"delta":{"content":"!"},"index":0}]}
data: {"id":"chat-123","choices":[{"finish_reason":"stop","index":0}]}
data: [DONE]
客户端解析器通过检测 data: 前缀识别有效数据行,使用 vim.json.decode() 解析 JSON 内容,遇到 [DONE] 标记时结束流式处理。
在实际调用 GLM-5 模型时,收到的响应流中出现了不符合 SSE 规范的裸文本数据:
data: {"id":"chat-456","choices":[{"delta":{"content":"请查看"},"index":0}]}
data: {"id":"chat-456","choices":[{"delta":{"tool_calls":[{"id":"call_1","function":{"name":"read_file","arguments":""},"index":0}]}]}
ts from you under
this License for any purpose whatsoever, with or without
data: {"id":"chat-456","choices":[{"delta":{"tool_calls":[{"function":{"arguments":"\"--filepath\""},"index":0}]}]}
data: {"id":"chat-456","choices":[{"finish_reason":"tool_calls","index":0}]}
data: [DONE]
可以看到在第 2 行和第 3 行之间,插入了两行没有 data: 前缀的纯文本内容。这些文本实际上是 Tool Call 的 arguments 参数值的一部分,应该是 GPL 许可证文本的片段。
以上是使用 openai 协议进行请求,切换到 anthropic 协议进行请求,比如发送一句写一篇1200个单词的文章到 hello.txt
data:{"type":"content_block_start","content_block":{"name":"write_file","input":{},"id":"toolu_tool-5f54e710c4364f5b9a5d0cafb65f246d","type":"tool_use"},"index":2}
data:{"delta":{"partial_json":"","type":"input_json_delta"},"type":"content_block_delta","index":2}
tracking every change. Integrated development environments provide sophisticated features like syntax highlighting, code completion, and debugging tools that enhance productivity. Testing frameworks help ensure code correctness, while continuous integration systems automate the process of building and deploying software.\\n\\nThe methodology of software development has also evolved significantly. The waterfall model, with its sequential phases of requirements, design, implementation, testing, and deployment, gave way to more flexible approaches like Agile and DevOps. These methodologies recognize that software development is inherently iterative, that requirements change, and that the ability to adapt quickly is more valuable than rigid adherence to an initial plan. The rise of microservices architecture, containerization, and cloud computing has further transformed how we build and deploy applications, emphasizing modularity, scalability, and resilience.\\n\\nBut programming is not just about tools and techniques; it is fundamentally about problem-solving. Every program begins with a problem, a gap between what is and what should be. The programmer's task is to bridge this gap, to transform a vague desire into a concrete solution. This transformation requires analytical thinking, the ability to break complex problems into smaller, manageable pieces, and creative synthesis, the skill of assembling these pieces into a coherent whole that addresses the original problem.\\n\\nThe cognitive processes involved in programming are fascinating and still not fully understood. Studies have shown that expert programmers use mental models to understand code, building representations of program state and simulating execution in their minds. They recognize patterns, draw on past experiences, and employ heuristics to navigate the vast space of possible solutions. This expertise develops over time, through thousands of hours of practice, and involves not just technical knowledge but also domain understanding, communication skills, and the ability to work effectively in teams.\\n\\nDebugging exemplifies the problem-solving nature of programming. When a program does not behave as expected, the programmer must become a detective, gathering evidence, forming hypotheses, and testing them systematically. This process requires both deductive reasoning—working from general principles to specific conclusions—and inductive reasoning—inferring general patterns from specific observations. It demands patience, attention to detail, and the humility to recognize that our initial assumptions are often wrong.\\n\\nThe social dimension of programming has grown increasingly important. Open-source software development demonstrates how communities can collaborate on a global scale, contributing code, documentation, and support to projects that benefit millions. Platforms like GitHub and GitLab have made it easier than ever to share code, review contributions, and coordinate efforts across time zones and languages. The open-source movement has not only produced valuable software but has also established new norms of transparency, collaboration, and shared ownership.\\n\\nProgramming also raises profound philosophical questions about the nature of intelligence, creativity, and agency. As we develop more sophisticated artificial intelligence systems, we must grapple with questions about whether machines can truly think, create, or understand. The code we write increasingly influences critical decisions in healthcare, finance, criminal justice, and other domains, raising ethical questions about bias, accountability, and transparency. The responsibility that comes with this power requires programmers to consider not just what they can do, but what they should do.\\n\\nLearning to program is a journey that never truly ends. The field evolves rapidly, with new languages, frameworks, and tools emerging constantly. Staying current requires continuous learning, a willingness to embrace new paradigms, and the humility to recognize that there is always more to learn. Yet the fundamentals—algorithmic thinking, abstraction, modularity, testing—remain remarkably stable. Mastering these fundamentals provides a foundation that enables adaptation to whatever new technologies emerge.\\n\\nFor those beginning this journey, the path may seem daunting. The syntax alone can be overwhelming, and error messages are often cryptic. But with persistence and practice, the fog begins to lift. Lines of code that once seemed impenetrable become readable, even elegant. Problems that seemed impossible become manageable challenges. The joy of solving a difficult problem, of seeing a program run correctly after hours of debugging, of building something that helps others—these are the rewards that keep programmers engaged.\\n\\nIn conclusion, programming is a multifaceted discipline that combines technical skill with creative thinking, individual effort with collaborative practice, and practical problem-solving with philosophical reflection. It is a craft that rewards patience, curiosity, and persistence. Whether you are writing a simple script to automate a tedious task or building a complex distributed system that serves millions of users, the fundamental challenges and joys of programming remain the same. It is about translating human intentions into machine actions, about building tools that extend our capabilities, and about participating in the ongoing project of shaping the digital world we all inhabit.\"}","type":"input_json_delta"},"type":"content_block_delta","index":2}
data:{"type":"content_block_stop","index":2}
上述是 anthropic 协议请求时的返回内容,第三行文字是无效数据。
通过对多次请求的原始响应进行比对,总结出以下异常特征:
| 特征 | 表现 | 出现频率 |
|---|---|---|
| 裸文本行 | 无 data: 前缀的纯文本 |
每次 Tool Call 必现 |
| 内容来源 | Tool Call arguments 字符串片段 | 100% |
| 出现位置 | tool_calls delta 输出过程中 | 100% |
| content 输出 | 正常 SSE 格式 | 不受影响 |
| finish_reason | 正确返回 tool_calls | 不受影响 |
| JSON 完整性 | tool_call JSON 被截断 | 100% |
这些特征表明问题具有高度一致性,并非网络传输错误或偶发异常,而是服务端实现层面的系统性问题。
该问题的本质是服务端在流式输出过程中,存在两条独立的输出通道被错误地混合写入同一个 HTTP 响应流:
| 输出类型 | 预期通道 | 实际通道 | 状态 |
|---|---|---|---|
| 普通内容 (content) | SSE JSON 封装 | SSE JSON 封装 | ✅ 正常 |
| Tool Call 参数 (arguments) | SSE JSON 封装 | 原始 stdout 直写 | ❌ 异常 |
正常情况下,所有输出内容都应当经过 SSE 协议封装层,统一格式化为 data: {...} 后写入 HTTP 响应体。但实际实现中,Tool Call 的 arguments 生成逻辑绕过了封装层,直接将 token 写入输出流。
根据输出特征,推测服务端可能存在类似以下的错误实现:
// ❌ 错误实现示意
func streamToolCallArguments(tokens []string, writer http.ResponseWriter) {
for _, token := range tokens {
// 错误:直接写入原始 token,未经过 SSE 封装
writer.Write([]byte(token))
writer.(http.Flusher).Flush()
}
}
// ✅ 正确实现应当是
func streamToolCallArguments(tokens []string, writer http.ResponseWriter) {
for _, token := range tokens {
// 正确:构造完整的 SSE 消息
message := SSEMessage{
Choices: []Choice{
{
Delta: Delta{
ToolCalls: []ToolCall{
{
Function: Function{
Arguments: token,
},
},
},
},
},
},
}
fmt.Fprintf(writer, "data: %s\n\n", json.Marshal(message))
writer.(http.Flusher).Flush()
}
}
观察到的裸文本如 ts from you under 实际上是 arguments 字符串的中间片段。这是因为:
例如,当 arguments 应该是 "GPL License text from you under this License" 时,实际输出可能是:
data: {"arguments":"GPL Licen"} // 正常 SSE
se text from you under // 裸文本(错误)
this License" // 裸文本(错误)
data: {"arguments":""} // 正常 SSE
从软件工程角度分析,这种问题通常由以下原因导致:
chat.nvim 的 SSE 解析器期望每一行输入都是合法的 data: {...} 格式,当遇到裸文本时会出现以下问题:
-- 简化的解析逻辑
local function parse_sse_line(line)
local data = line:sub(7) -- 去掉 "data: " 前缀
return vim.json.decode(data) -- 裸文本会导致这里报错
end
当输入行为 ts from you under 时:
line:sub(7) 返回 ` from you under`(或原样返回,如果行长度不足 7)vim.json.decode() 尝试解析非 JSON 字符串,抛出异常| 影响项 | 严重程度 | 说明 |
|---|---|---|
| JSON 解析失败 | 高 | vim.json.decode() 抛出异常 |
| Tool Call 识别失败 | 高 | arguments 内容不完整,无法调用工具 |
| 流式输出中断 | 中 | 可能触发错误处理逻辑,提前终止 |
| 用户体验下降 | 中 | 功能不可用或响应异常 |
| 错误日志污染 | 低 | 产生大量解析错误日志 |
在无法修改服务端的情况下,客户端可以增加容错逻辑,过滤非法数据行:
local function safe_parse_sse(line)
-- 严格过滤:只处理以 "data: " 开头的行
if not line:match("^data: ") then
vim.log.warn("跳过非法 SSE 行:" .. line)
return nil
end
local ok, result = pcall(function()
local content = line:sub(7)
if content == "[DONE]" then
return "DONE"
end
return vim.json.decode(content)
end)
if not ok then
vim.log.warn("JSON 解析失败:" .. result)
return nil
end
return result
end
-- 在 on_stdout 回调中使用
on_stdout = function(_, data)
for line in data:gmatch("[^\n]+") do
local parsed = safe_parse_sse(line)
if parsed then
handle_chunk(parsed)
end
end
end
该方案的核心思想是严格过滤 + 静默丢弃,遇到非法数据行时记录警告日志但不中断处理流程。这样可以保证在服务端修复问题之前,客户端仍能正常工作。
使用 curl 直接请求并保存完整响应,便于离线分析:
curl -N \
https://coding.dashscope.aliyuncs.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "glm-5",
"messages": [
{"role": "user", "content": "请读取当前目录下的 LICENSE 文件"}
],
"stream": true,
"tools": [
{
"type": "function",
"function": {
"name": "read_file",
"parameters": {
"type": "object",
"properties": {
"filepath": {"type": "string"}
}
}
}
}
]
}' \
> raw_response.txt
参数说明:
-N:禁用缓冲,实时输出raw_response.txt:保存完整原始响应# 统计总行数
wc -l raw_response.txt
# 查看非 data: 开头的行(非法行)
grep -v "^data: " raw_response.txt
# 统计非法行数量
grep -cv "^data: " raw_response.txt
# 查看非法行的上下文(前后各 2 行)
grep -v "^data: " -B 2 -A 2 raw_response.txt
# 提取所有 data: 行并验证 JSON 格式
grep "^data: " raw_response.txt | \
sed 's/^data: //' | \
while read line; do
echo "$line" | jq . 2>/dev/null || echo "INVALID: $line"
done
# 提取所有 tool_calls 相关的行
grep "tool_calls" raw_response.txt
# 检查 arguments 字段是否完整
grep "arguments" raw_response.txt | \
sed 's/^data: //' | \
jq '.choices[0].delta.tool_calls[0].function.arguments'
如果 arguments 字段显示不完整或解析失败,说明存在数据污染。
该问题属于服务端协议层实现缺陷,具体表现为:
Tool Call 的 arguments 在流式生成过程中未被正确封装进 SSE 消息格式,导致原始 token 直接泄漏到 HTTP 响应流中。这反映了服务端在流式输出模块和 Tool Call 模块的集成测试不足。
对服务端(阿里云):
对客户端(chat.nvim):
建议将原始响应日志提交给阿里云技术支持,附上:
raw_response.txt 文件最近在使用 job.nvim 执行一些命令时,遇到了一个奇怪的问题。当我执行一个返回 JSON 数据的命令时,发现 JSON 数据无法被正确解析,总是报错 unexpected end of JSON。经过调试后发现,问题出在 job.nvim 的 raw 参数默认值上。
假设我们有一个命令,它会输出一个较大的 JSON 数据:
local job = require('job')
local result = {}
local jobid = job.start({ 'some-command-that-outputs-json' }, {
on_stdout = function(id, data)
for _, line in ipairs(data) do
table.insert(result, line)
end
end,
on_exit = function(id, code, signal)
local json_str = table.concat(result, '')
local ok, decoded = pcall(vim.json.decode, json_str)
if not ok then
print('JSON decode failed: ' .. decoded)
else
print('JSON decoded successfully')
vim.print(decoded)
end
end,
})
执行上述代码后,可能会看到类似以下的错误:
JSON decode failed: Expected object key string but found null at character 1024
或者在解析时直接报错:
JSON decode failed: Expected value but found T_END at character 1
要理解这个问题,需要先了解 job.nvim 的 raw 参数的作用。查看 job.nvim 的源码,可以看到:
--- @class JobOpts
--- @field on_stderr? function
--- @field on_exit? fun(id: integer, code: integer, signin: integer)
--- @field on_stdout? function
--- @field cwd? string
--- @field detached? boolean
--- @field clear_env? boolean
--- @field env? table<string, string|number>
--- @field encoding? string
--- @field raw? boolean
raw 参数默认是 nil(即 false),这意味着使用行缓冲模式。在这种模式下,数据会通过 buffered_data 函数处理:
---@param eof string
---@param data string
local function buffered_data(eof, data)
data = data:gsub('\r\n', '\n')
local std_data = vim.split(data, '\n')
if #std_data > 1 then
std_data[1] = eof .. std_data[1]
eof = std_data[#std_data] == '' and '' or std_data[#std_data]
table.remove(std_data, #std_data)
elseif #std_data == 1 then
if std_data[1] == '' and eof ~= '' then
std_data = { eof }
eof = ''
elseif std_data[1] == '' and eof == '' then
std_data = {}
elseif std_data[#std_data] ~= '' then
eof = std_data[#std_data]
std_data = {}
end
end
return eof, std_data
end
这个函数的逻辑是:
\r\n 替换为 \n\n 分割数据eof 变量中等待下一次数据问题在于:大多数 JSON 输出是不包含换行符的单行数据,或者是一个格式化的多行 JSON。当 JSON 数据较大时,可能会被系统分成多个数据块(chunk),但每个数据块内部可能没有换行符。
在这种情况下,buffered_data 函数会:
eof 中std_data 是空的,不会触发 on_stdout 回调这样就导致了数据在缓冲区中累积,但 on_stdout 回调可能不会及时接收到数据。更糟糕的是,如果 JSON 数据本身就包含换行符(格式化的 JSON),会被错误地分割成多行。
-- Default: line-buffered mode
uv.read_start(stdout, function(_, data)
if data then
local stdout_data
_jobs['jobid_' .. current_id].state.stdout_eof, stdout_data =
buffered_data(
_jobs['jobid_' .. current_id].state.stdout_eof,
data
)
if #stdout_data > 0 then
vim.schedule(function()
if opts.encoding then
stdout_data = vim.tbl_map(function(t)
return vim.fn.iconv(t, opts.encoding, 'utf-8')
end, stdout_data)
end
opts.on_stdout(current_id, stdout_data)
end)
end
return
end
-- ... EOF handling
end)
-- Raw mode: no buffering, pass raw data chunks directly
if opts.raw then
uv.read_start(stdout, function(_, data)
if data then
vim.schedule(function()
if opts.encoding then
data = vim.fn.iconv(data, opts.encoding, 'utf-8')
end
if nparams == 2 then
opts.on_stdout(current_id, { data })
else
opts.on_stdout(current_id, { data }, 'stdout')
end
end)
else
-- EOF
if stdout and not stdout:is_closing() then
stdout:close()
end
end
end)
end
可以看到,raw = true 模式下:
针对 JSON 数据解析的场景,有以下几种解决方案:
local job = require('job')
local result = {}
local jobid = job.start({ 'some-command-that-outputs-json' }, {
raw = true, -- 使用原始模式
on_stdout = function(id, data)
-- data 是一个包含原始数据块的列表
for _, chunk in ipairs(data) do
table.insert(result, chunk)
end
end,
on_exit = function(id, code, signal)
local json_str = table.concat(result, '')
local ok, decoded = pcall(vim.json.decode, json_str)
if not ok then
print('JSON decode failed: ' .. decoded)
else
print('JSON decoded successfully')
vim.print(decoded)
end
end,
})
如果 JSON 数据是单行输出(minified JSON),需要在 on_exit 回调中处理缓冲区中剩余的数据:
local job = require('job')
local result = {}
local jobid = job.start({ 'some-command-that-outputs-json' }, {
on_stdout = function(id, data)
for _, line in ipairs(data) do
-- 如果 JSON 是格式化的多行,需要保留换行符
table.insert(result, line)
end
end,
on_exit = function(id, code, signal)
-- 拼接所有行,如果是 minified JSON,不需要换行符
-- 如果是格式化 JSON,需要添加换行符
local json_str = table.concat(result, '\n')
local ok, decoded = pcall(vim.json.decode, json_str)
if not ok then
print('JSON decode failed: ' .. decoded)
else
print('JSON decoded successfully')
vim.print(decoded)
end
end,
})
如果需要更精细的控制,可以直接使用 vim.loop (或 vim.uv) 的 stream API:
local uv = vim.uv or vim.loop
local stdout = uv.new_pipe()
local result = {}
local handle, pid = uv.spawn('some-command', {
stdio = { nil, stdout, nil },
}, function(code, signal)
stdout:close()
handle:close()
local json_str = table.concat(result, '')
local ok, decoded = pcall(vim.json.decode, json_str)
if not ok then
print('JSON decode failed: ' .. decoded)
else
print('JSON decoded successfully')
vim.print(decoded)
end
end)
uv.read_start(stdout, function(err, data)
if data then
table.insert(result, data)
end
end)
在我开发的 chat.nvim 插件中,调用 AI API 时返回的就是 JSON 数据。最初使用默认模式时,遇到了数据解析失败的问题。
查看调试日志:
[ 14:23:42:123 ] [ Info ] Received chunk 1: {"id":"chatcmpl-123","object":"chat.completion","created":1234567890
[ 14:23:42:125 ] [ Info ] Received chunk 2: ,"model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant"
[ 14:23:42:127 ] [ Info ] Received chunk 3: ,"content":"Hello"},"finish_reason":"stop"}]}
可以看到,JSON 数据被分成了 3 个数据块,每个块都不完整。在默认模式下,这些数据会被缓冲,等待换行符,但 JSON 数据中没有换行符,导致数据一直在缓冲区中。
使用 raw = true 后,可以正确地接收并拼接这些数据块:
local job = require('job')
local chunks = {}
job.start(cmd, {
raw = true,
on_stdout = function(id, data)
for _, chunk in ipairs(data) do
table.insert(chunks, chunk)
end
end,
on_exit = function(id, code, signal)
local json_str = table.concat(chunks, '')
-- 现在可以正确解析 JSON
local response = vim.json.decode(json_str)
-- 处理响应数据
end,
})
job.nvim 的 raw 参数默认为 false,使用行缓冲模式,适合处理以行为单位的文本输出(如日志、命令输出等)。但如果要处理二进制数据或单行的大型 JSON 数据,应该设置 raw = true,然后在回调函数中手动拼接数据块。
理解 raw 参数的作用对于正确处理外部命令的输出非常重要:
在使用 job.nvim 处理 JSON 数据时,建议使用 raw = true 并在 on_exit 回调中拼接所有数据块后再进行解析。
job.nvim 发布了 1.5 版本,和往常一样,我在 Reddit 上发布了版本更新文章,也很感谢有不少正面的反馈。
但让我无法理解的是,有些人似乎很难沟通,他们一再强调 Neovim 有内置的 jobstart() 和 vim.system(),为什么还要创建一个新的插件?即使解释了原因,他们仍然无法理解。我想这可能是我说得不够清楚,因此在这里整理一下前因后果。
说起 Job 函数,要从最早期的 Neovim 版本说起。Neovim 增加了一个 jobstart() Vim Script 函数,那时候 Vim 还没有 Job 功能,后来 Vim 才增加了 job_start() 函数,但其调用方式与 Neovim 并不一致。早期的 Neovim 主要还是以使用 VimL 为主。
于是我给 SpaceVim 添加了一个 Job API,以兼容早期 Neovim 的 jobstart() 和 Vim 的 job_start() 函数。
commit 44ad1cb4fe6a8d9ccae49b71994e6182bbcaa968
Author: wsdjeg <[email protected]>
Date: Fri Mar 31 21:09:38 2017 +0800
Add job api for vim and neovim
这样,就可以使用相同的函数同时兼容 Vim 和 Neovim。
let s:JOB = SpaceVim#api#import('job')
let s:command = ['echo', 'hello world']
function! s:stdout(id, data, event)
" data 是一个字符串列表
for line in a:data
echo line
endfor
endfunction
call s:JOB.start(s:command, {
\ 'on_stdout' : function('s:stdout'),
\ }
\ )
在这个过程中,前后还遇到过很多兼容性问题。Neovim 的 jobstart() 函数的 stdout callback 在数据过大时会被截断,具体的 buffer size 我记不清了,有个 issue 讨论过这个问题。于是,我在这个 API 中增加了 data_eol 检测,以确保 callback 函数被调用时传入的是完整的数据。当然,后来的 Neovim 官方文档里也写了如何处理这种数据被截断的情况,详见 :h channel_buffered。
随着 Neovim 对 Lua 的支持越来越多,我后来使用 Lua 重写了 Job API,但仍然将其内置在 SpaceVim 里。重写之后,调用就可以直接使用 Lua 了。
commit 879129388ab22b64c5a5cf0df83799084cab96fc
Author: Eric Wong <[email protected]>
Date: Wed Jul 5 22:58:01 2023 +0800
feat(api): add lua job api
close https://github.com/neovim/neovim/issues/20856
调用方式变成了 Lua:
local job = require('spacevim.api.job')
local jobid = job.start(vim.g.test_ctags_cmd, {
on_stdout = function(id, data, event)
vim.print(id)
vim.print(data)
vim.print(event)
end,
on_stderr = function(id, data, event)
vim.print(id)
vim.print(data)
vim.print(event)
end,
on_exit = function(id, code, signal)
vim.print(id)
vim.print('exit code', code)
vim.print('exit signal', signal)
end,
})
随着 SpaceVim 项目停止维护,我把我常用的功能插件独立成了各个单独的 Neovim 插件,其中就包括了 job.nvim。
我自己写的很多需要异步执行命令的 Neovim 插件都依赖这个 job.nvim,这样就不需要在每个插件仓库里单独维护执行外部命令的模块了。使用起来也比原来的 SpaceVim 内置 Lua Job API 更简洁一些:
local job = require('job')
local function on_exit(id, code, signal)
print('job ' .. id .. ' exit code:' .. code .. ' signal:' .. signal)
end
local cmd = { 'echo', 'hello world' }
local jobid1 = job.start(cmd, {
on_stdout = function(id, data)
vim.print(data)
end,
on_exit = on_exit,
})
vim.print(string.format('jobid is %s', jobid1))
local jobid = job.start({ 'cat' }, {
on_stdout = function(id, data)
vim.print(data)
end,
on_exit = function(id, code, signal)
print('job ' .. id .. ' exit code:' .. code .. ' signal:' .. signal)
end,
})
job.send(jobid, { 'hello' })
job.chanclose(jobid, 'stdin')
我不太记得 vim.system 是什么时候加入到 Neovim 的了,其前后应该也功能迭代过几个版本。为什么我还在继续维护 job.nvim 而不切换到 vim.system 呢?
没有对 stdout 数据进行拼接处理,容易有截断数据,而且传给 callback 函数的数据是 string 而非像 jobstart 那样是 string 列表。当然,数据类型都是次要的事情,使用 split 函数很容易得到列表,但是未做数据拼接这点,在写 callback 函数时会增加很多额外的代码量。
callback 函数内无法确认到底是哪个 job 调用触发的这个 callback 函数,应该像 jobstart() 的 stdout callback 函数那样,传入一个 jobid 参数。
我创建 job.nvim 的主要原因是:
在旧的 Neovim 版本中,没有 vim.system。第一个版本是 job.vim,它使用 VimL,并以与 Neovim 的 jobstart 相同的 API 支持 Neovim 和 Vim。
我需要为不同的 job 的 stdout 使用相同的 callback 函数。例如,在我的插件管理器 https://github.com/wsdjeg/nvim-plug 中,当同时克隆 8 个插件时,我需要在 job 退出前显示每个 job 的进度。因此在 stdout callback 函数中,我需要知道是哪个 job 触发了这个 callback 函数。据我所知,即使现在的 vim.system 的 stdout callback 也不支持这个功能。
两种写法,哪种更简单方便,一目了然:
使用 vim.system:
local function on_stdout(err, data)
--- 首先,这里需要对 data 判断数据的完整性,然后参考以下鬼方法来拼接:
-- There are two ways to deal with this:
-- - 1. To wait for the entire output, use |channel-buffered| mode.
-- - 2. To read line-by-line, use the following code: >vim
-- let s:lines = ['']
-- func! s:on_event(job_id, data, event) dict
-- let eof = (a:data == [''])
-- " Complete the previous line.
-- let s:lines[-1] .= a:data[0]
-- " Append (last item may be a partial line, until EOF).
-- call extend(s:lines, a:data[1:])
-- endf
-- 然后,拼接完成后,再执行逐行提取
for _, line in ipairs(data) do
local progress = string.match(line, '%d*%%')
-- 然后在这个地方,你就会发现,无法判断这到底是哪个 Job 触发的 callback 函数了。
end
end
-- clone plugin A
vim.system({ 'git', 'clone', url_a }, { stdout = on_stdout })
-- clone plugin B
vim.system({ 'git', 'clone', url_b }, { stdout = on_stdout })
使用 job.nvim:
local job = require('job')
local jobs = {}
local function on_stdout(id, data)
for _, line in ipairs(data) do
print(
string.format(
'repo %s clone progress %s',
jobs[id],
string.match(line, '%d*%%')
)
)
end
end
-- clone plugin A
local id1 = job.start({ 'git', 'clone', url_a }, {
on_stdout = on_stdout,
})
jobs[id1] = 'A'
-- clone plugin B
local id2 = job.start({ 'git', 'clone', url_b }, {
on_stdout = on_stdout,
})
jobs[id2] = 'B'
也许,job.nvim 在许多年后会停止维护,那一定是我找到了更合适的内置替代方案。至少目前,vim.system 的实现还没有完全满足我的使用需求。
最后,我终于理解 avante.nvim 的作者为什么删掉 Reddit 账号了。吵架真的很烦。 https://www.reddit.com/r/neovim/comments/1rdgfxg/comment/o7bzfzc/
vim.system 的设计本身就有问题。我自己写了将近二三十个异步调用命令的插件,难道我遇到的问题还不够多吗?有些人一再给我强调可以在 exit_cb 里面区分 job,难道调用常驻命令时,要等它们执行完毕才能看到结果吗?
那就让我们看看以后的版本 vim.system 会不会增加这样的参数传入,或者会不会有类似的新的内置函数出现吧。