1. 先看现象:这次事故到底发生在哪个环境里
1.1 背景交代:内网开发机 + Antigravity
要说这两周最让我血压飙升的事,莫过于手头那台内网开发机上的 Antigravity 突然连不上 SSH 了。本来一切正常——早上到工位,启动 IDE,打开 Remote SSH 窗口,连到 Linux 开发机继续改代码,结果这次直接卡在认证阶段,反复报 Permission denied。
先交代一下环境。开发机是典型的公司内网服务器,系统是 CentOS Stream 9,本地则是装在 Windows 工作站上的 Antigravity,两侧只能走内网,外网完全不通。平时这套组合很省心,代码放远程、编译放远程、AI 辅助也直接读远程上下文。但正因为太省心,我对它底层怎么拉取远程组件这件事基本没上过心。
等到连接真的报错,我才意识到:Antigravity 这类 AI 编程 IDE 的远程开发能力,本质上还是建立在 SSH 通道之上的。SSH 一旦断掉,上层所有功能——文件同步、远程终端、AI 代理——全部跟着瘫痪,错误提示也特别具有迷惑性。这篇文章就把我这次在内网/离线环境下定位 SSH 连接失败、以及误删手动安装包之后怎么恢复的完整过程记录下来,方案已经实测有效,照着走能省掉很多弯路。
1.2 报错实录:三种异常信息指向不同层级
这次连接失败,我先后遇到过三句不同的报错,这里先列出来,方便对照:
Could not establish connection to host: Permission denied (publickey),最常见的认证失败,服务端直接拒绝公钥。Agent terminated due to error,Antigravity 里 AI 代理在底层连接断开后抛出的错误,看着像是在报 AI 模型的问题,其实根因在 SSH。Connection closed by remote host,连接建立后被远端立刻掐断,多与服务端配置、防火墙策略有关。
这三种报错都叫“连不上”,但排查方向完全不同。第一种要查客户端密钥和服务端 authorized_keys;第二种要查远程服务端组件是否完整;第三种要查端口、sshd 配置和日志。如果一上来就盲目重装 SSH 服务,大概率白费时间。
1.3 一次错误操作:把救命的安装包删了
在排查过程中,我从同事的备份盘里拿了一个离线安装包,里面装的是 Antigravity 远程开发必需的组件。本来该把它妥善归档,结果那会儿人急了,临时解压到了系统 /tmp 目录,还在清理临时文件夹时顺手给删了。
删除后的最初几分钟我还没太慌,觉得顶多重新拷一次。然而内网环境下,这个安装包项目组里一共就两份,一份在同事手里,另一份就是我刚删掉的这个。等真正需要它来解决远程服务端组件缺漏时,我才意识到问题的严重性。这里也提醒大家一句:离线环境里的手动安装包就是“救命粮”,放 /tmp 或者下载目录这种会被随手清理的位置,几乎等于埋雷。这次误删,直接把我的恢复路径从“定位问题再修复”升级成了“先找回安装包再修复”,难度凭空翻了一倍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么内网/离线环境更容易触发这些问题
2.1 内网网络结构的三个特殊点
在内网环境里排查 SSH 问题,和公网完全是两个思路。第一个特殊点是端口经常不是天然放行的。不少办公楼的内网对主机互访有白名单策略,22、2222 这类端口不定制放行,连接请求会被防火墙静默丢弃,客户端看到的只是超时或者拒绝,根本猜不到是端口问题。
第二个特殊点是域名解析受限。内网机器之间常用主机名互相访问,靠的是内网 DNS 或者 hosts 文件映射。一旦 hosts 条目写错,SSH 请求会被引导到错误主机,报错依旧是“连接失败”,非常容易让人绕进死胡同。
第三个特殊点最致命:外网不可达,导致所有在线下载彻底失效。开发工具为了方便,默认把依赖放在公网源里,远程开发扩展的远端服务端、AI 推理组件都需要联网拉取。离线环境下自动下载必败,IDE 往往只是给出一个模糊的“连接失败”,不会告诉你它真正卡在下依赖这个环节。理解了这三件事,再去看那些莫名其妙的报错就顺了。
2.2 离线模式下远程开发依赖的缓存路径
再深入一层,说说远程开发的真实依赖链。Antigravity 的 Remote SSH 机制不是简单把文件拉到本地编辑,而是先在远程机器上部署一个服务端程序,再通过 SSH 通道把文件更新、命令执行都走一遍远程。换句话说,远程机器上必须存在一个能跑起来的服务端组件。
公网环境里,这个组件由 IDE 首次连接时自动下载部署;离线环境里自动下载必然失败,只能手动把服务端组件放到远程指定目录。这就是“手动安装包”存在的根本理由。
实际操作中,依赖通常分散在两个地方:本地 IDE 的扩展目录,承载 SSH 扩展本体;远程主机的用户缓存目录,比如 ~/.cache、~/.vscode-server 这类位置,承载服务端运行文件。任何一个位置缺失,连接就会报错。我之前遇到的 Agent terminated due to error,本质就是远程服务端组件缺了,SSH 隧道虽然建立了,但服务端起不来,代理程序自然跟着终止。
2.3 Antigravity 为什么把 SSH 问题放大成 AI 功能失效
Antigravity 的核心亮点是 AI 辅助能力,它会在本地、远程、模型服务之间搭建一条完整通道。SSH 在这里承担的角色不只是传文件,而是整条远程工作区的生命线。底层通信一断,最上层的 AI 代理立刻终止,所以你能看到一句很诡吊的 Agent terminated due to error。
这就解释了一个现象:很多人以为 SSH 连接失败和 AI 功能失效是两件事,其实是一条链路的两端。我在这次事故里最大的认知变化,就是学会了把“SSH 修复”当作“AI 工作流恢复”的前置步骤。不要想着绕开 SSH 单独救 AI,那是白费力气。
3. SSH 连接失败的完整排查与修复流程
3.1 从网络到认证逐层收窄:排查顺序很关键
排查 SSH 问题,最忌乱试。我自己的习惯是画一条链路,从客户端到服务端逐层验证,每一层都只有“通”和“不通”两个结果:
ping 目标IP,确认网络层是否可达。nc -vz 目标IP 22或telnet 目标IP 22,确认目标端口能建立 TCP 连接。ssh -vvv user@目标IP,用详细日志方式连接,看认证过程卡在哪一步。- 根据上一步输出决定查密钥、服务端配置还是网络策略。
三层里只要有一层不通,上层一定报连接失败。实际操作时,我见过太多人跳过端口检查直接折腾密钥,最后发现罪魁祸首是白名单策略没放行 22 端口。顺序对了,十分钟就能定位,乱试可能两小时都出不来。
3.2 服务端检查:sshd 状态、配置和日志
进入服务端之后,重点看三样东西。第一,SSH 服务是否真的在运行,用 systemctl status sshd 或 service ssh status 查看。内网机器有时会因资源不足而异常退出,现象就是客户端莫名其妙连不上。
第二,/etc/ssh/sshd_config 的核心配置项。这里我最关注四个字段:PubkeyAuthentication 是否 yes,PasswordAuthentication 是否按需开启,AllowUsers 有没有把当前用户排除掉,Port 是否被改成非默认端口。尤其是 AllowUsers 白名单,如果用户不在里面,密钥再正确也连不上,而且报错并不直观。
第三,日志。多数发行版会把 SSH 登录记录写到 /var/log/secure 或 /var/log/auth.log,也可以直接 journalctl -u sshd。日志里能看到最原始的拒绝原因,比如 Failed publickey 还是 Connection closed by authenticating user。我这次能快速定位到客户端密钥问题,就是因为日志里清楚地写着公钥认证失败,直接跳过了服务端的大量排查。
3.3 客户端检查:密钥、known_hosts 和 config
客户端侧最常出问题的三个文件,一个一个说。首先是私钥文件,默认路径是 ~/.ssh/id_ed25519 或 ~/.ssh/id_rsa。如果私钥不存在、权限过于开放,或者密钥与服务端记录不匹配,都会被拒绝。快速验证方式是在客户端执行:
bash复制ssh-keygen -y -f ~/.ssh/id_ed25519
这条命令会从私钥推导出公钥,和服务端 ~/.ssh/authorized_keys 里的内容比对一下就知道配没配对。
第二个是 known_hosts 文件。如果远程主机重装过系统或者密钥轮换过,本地还记着旧指纹,就会报 Host key verification failed。处理方式是用 ssh-keygen -R 目标IP 删除旧记录,再重新连接。
第三个是 config 文件。内网连接建议把所有参数显式写好,我的实际配置长这样:
bash复制Host devbox
HostName 10.10.20.35
User dev
Port 22
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
ServerAliveInterval 60
这里 IdentitiesOnly yes 特别重要,它避免 ssh 把默认目录下所有密钥挨个尝试一遍,导致服务端策略触发大批量拒绝。ServerAliveInterval 则是给连接加心跳,避免长期空闲被服务端断开。
3.4 Antigravity 里的针对性配置:离线插件与远程设置
确认命令行 SSH 已经能连上之后,再回到 Antigravity 做针对性配置。这一阶段解决两个核心问题。
第一,让远程扩展组件不再走自动下载。在 IDE 的扩展设置里,把 Remote SSH 相关扩展改为本地安装模式,确保扩展目录完整存在于本地扩展根目录,并手动指定系统里的 ssh 可执行文件路径。部分版本还支持指定远程服务端的安装目录,这正好配合离线场景。
第二,打开连接过程日志。不同版本的设置项名称略有差异,常见的是 remote.SSH.showLoginTerminal,开启后每次连接都会弹出一个终端窗口,把 SSH 执行过程完整打出来。这个功能在排查阶段极其好用,状态栏里一句“连接失败”根本看不出问题,终端里的详细信息却能直接指向故障层级。
4. 手动安装包被误删后的恢复方案
4.1 先找:离线安装包都藏在哪些路径
手动安装包被删之后,第一反应不是焦虑,而是去固定位置搜索残留。我这次实际排查了三个地方。
第一个是 IDE 扩展目录。如果安装包是通过“从 VSIX 安装”的方式装进 IDE 的,扩展目录里往往会保留解压后的组件文件夹,甚至可能有 .vsix 文件缓存。在类 VSCode 架构的产品里,这个目录通常叫 ~/.antigravity/extensions 或 ~/.vscode/extensions。
第二个是系统临时目录和下载目录。安装包如果解压到 /tmp 或 ~/Downloads,清理后仍有概率通过文件恢复工具找回,但概率有限。真正需要注意的是,很多“删除”只是删了外层目录,实际内容可能还残留在磁盘里,别急着覆盖写入。
第三个是公司内部共享资源。内网环境最大的好处是,内部文件通常有留存习惯。同事机器、共享盘、NAS,都是候选位置。我当时就是在一台同事的备份盘里找到了同一版本的包,省掉了重建的麻烦。通用搜索命令是:
bash复制find / -name "*.vsix" 2>/dev/null
只要磁盘没被大量覆盖,这条命令基本能发现所有残留安装包。
4.2 恢复组件:从扩展目录手工组装
如果找不到完整 .vsix,但扩展目录里有解压好的组件文件夹,我们还可以手动组装恢复。原理是 IDE 启动时不一定非要读取安装包文件本身,只要扩展目录结构完整、版本号匹配,就能正常加载。
操作要点如下:
- 把残留的扩展文件夹完整复制到一个安全目录,例如
~/ext-backup/。 - 检查目录下的
package.json,确认name、version、engines字段。 - 如果 IDE 扩展清单里缺失该扩展,就把整个文件夹放回扩展根目录,并保证文件夹命名符合“扩展名-版本号”规则。
- 重启 IDE,在扩展面板确认该扩展是否变为“已启用”。
这种方法对目录完整但文件损坏的安装尤其有效。如果目录里缺了 out、dist 这类编译产物,那只能找完整包重新装了。
4.3 重建安装包:从可联网机器导出再拷贝
内网机器不能联网,不代表整个项目组都没有联网环境。我们可以在一台符合相同架构且能联网的机器上,装好相同版本 IDE 和 Remote SSH 扩展,然后把整个扩展目录打包,再拷贝进内网。打包时有两个关键点。
第一,尽量保持目录相对路径一致。解压后如果依赖路径发生偏移,扩展运行时会大量报错,看起来像安装包坏了,其实是相对路径被破坏。
第二,验证包内是否包含平台对应的二进制文件。如果目标机器是 Linux x64,源导出机器也必须是 Linux x64。跨架构的包很可能缺对应可执行文件,装上去了也跑不起来。生成安装包参考命令:
bash复制tar -czf antigravity-remote-ssh-linux-x64.tar.gz -C ~/.antigravity/extensions 对应扩展目录
4.4 防误删:给离线资源建立“三重副本”习惯
这次事故之后,我把离线资源管理固定成了三个层级。第一层,本地专用目录,比如 D:\offline-packages,按工具和版本分子文件夹存放。第二层,部门共享文件服务器,只读权限对全体开发开放,写入权限只给维护者。第三层,保留至少一份离线冷备,放在不参与日常清理的移动硬盘或归档服务器里。
同时规定,安装包进入这些目录时,文件名必须包含版本号和来源说明,例如 remote-ssh-0.116.0-离线包.tar.gz。这一条看着简单,实际价值巨大。内网环境里,同一种工具往往有多个历史版本,IDE 升级后旧扩展可能不兼容,没有版本信息的包就是一堆废文件。三重副本加上规范命名,能根治“删了找不回”的问题。
5. 实战记录:从故障到恢复的完整过程(已验证有效)
5.1 这次恢复的整体路径设计
回到事故本身,我当时的处境是:SSH 连接失败 + 远程扩展手动安装包被误删。要恢复,必须串成一条完整路径,而不是零散修一步看一步。
阶段一:先用命令行确认底层 SSH 可用。如果命令行也连不上,就先修密钥和服务端配置。阶段二:找回离线安装包,找不到就重建,确保本地 IDE 扩展目录完整。阶段三:预置远程服务端组件,让 IDE 首次连接时不需要去外网拉取。阶段四:配置 Antigravity 连接参数,执行连接验证,再确认 AI 功能恢复正常。
这四步走完,我在 15 分钟内完成了全部恢复。下面把每一步的关键操作列清楚。
5.2 阶段一和阶段二:修复 SSH 认证 + 恢复扩展包
首先在客户端重新生成密钥并验证:
bash复制ssh-keygen -t ed25519 -C "dev-machine" -f ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub
把输出的公钥追加到远程 ~/.ssh/authorized_keys 之后,用这条命令验证:
bash复制ssh -i ~/.ssh/id_ed25519 dev@目标IP "echo ok"
终端出现 ok,说明 SSH 基础链路已通。接下来把找回的离线扩展包解压到 IDE 扩展根目录:
bash复制tar -xzf remote-ssh-linux-x64.tar.gz -C ~/.antigravity/extensions/
重启 IDE,到扩展面板确认已启用。这一步千万别省,否则 Antigravity 里依旧会提示连接失败,而你根本不知道扩展没加载。
5.3 阶段三:预置远程服务端组件
这是离线环境里最核心的一步。Remote SSH 类扩展首次连接时,会把服务端组件同步到远程机器。远程机器不能访问外网时,自动同步就会卡死。解决办法是提前把服务端组件放到远程缓存目录。
不同 IDE 目录名不一样,但原理一致。通常操作是:在本地扩展目录里找到类似 dist、server 的文件夹,用 scp 或 rsync 拷贝到远程:
bash复制scp -r 本地扩展服务器组件目录 dev@目标IP:~/.cache/antigravity-server/
然后用 IDE 的设置项 remote.SSH.serverInstallPath(对应版本字段名可能不同)指定这个路径,让 IDE 跳过自动下载。注意版本一致性:远程组件版本必须和本地扩展版本匹配,否则启动时依然会报错。这一步做完,离线连接成功率就会大幅提升。
5.4 阶段四:连接验证及 AI 功能恢复
在 Antigravity 的远程资源管理器里选择目标主机,用 ~/.ssh/config 里配置好的别名连接。本次实际验证结果如下:
- SSH 隧道建立:成功,命令行响应时间小于 100ms。
- 远程扩展加载:成功,不再出现
Agent terminated或Could not establish connection。 - AI 功能:远程上下文能正常读取,AI 代理重新进入可用状态。
理论上一台离线开发机的完整恢复时间应该在 20 分钟以内。只要网络策略没有被绑死,这条路径的普适性很强。如果你也遇到类似问题,建议直接按五阶段流程走一遍,比我当时边试边猜高效得多。
6. 常见问题速查与避坑经验
6.1 故障速查表
| 故障现象 | 可能原因 | 优先排查点 |
|---|---|---|
| Permission denied (publickey) | 客户端私钥与服务端公钥不匹配 | 重新生成密钥并导入 authorized_keys |
| Host key verification failed | known_hosts 指纹过期 | 用 ssh-keygen -R 删除旧指纹 |
| Connection closed by remote host | 服务端配置或防火墙策略拦截 | 查 sshd_config、/var/log/secure |
| Agent terminated due to error | SSH 断开导致上层 AI 代理终止 | 先恢复 SSH 基础链路再重开 IDE |
| IDE 提示无法自动下载组件 | 离线环境没有外网访问 | 预置远程服务端组件并指定安装路径 |
| 扩展已安装但未生效 | 版本不匹配或目录名不规范 | 检查 package.json 版本和扩展目录名 |
6.2 三条血泪教训
第一,排查 SSH 问题时永远先确认命令行可连。Antigravity 这类 IDE 会在 SSH 之上加封装,它的报错信息把真实原因掩盖得很深。命令行连接一旦成功,面板层的大半问题已经消失。
第二,离线安装包绝对不要放在会被“顺手清理”的位置。/tmp、下载目录、IDE 临时目录,全都不合格。按前一节说的三重副本习惯管理,能根除这类问题。
第三,不要在 SSH 报错时随手改服务端权限。网上很多教程会教你直接打开 PasswordAuthentication,甚至关闭密钥验证,这在公网环境是严重的安全隐患,在内网也会掩盖真正的故障点。分层排查虽然看起来多花了点时间,但每一步都有明确结果,反而是最快的路径。
其实回过头看,这次事故真正花时间的不是 SSH 修复本身,而是“安装包被误删后重新找回”的环节。如果早点把离线资源按版本、按用途分类备份,可能十几分钟就结束战斗。我在实际操作里还有一个很小但很有用的习惯:每次在内网机器上手动安装任何东西之前,都先执行一次目录文件清单,把原始包复制到独立的 offline-backup 文件夹。这样即便 IDE 扩展或服务端组件之后损坏,也不需要改造整个流程,只要从备份目录再复制一次就能恢复。这个方法我已经在项目组里推广了,如果你想长期在内网环境维护开发工具,强烈建议你也试一下。
