平常连接服务器时,我会先在堡垒机页面点击 SSH,由本地客户端打开 SecureCRT,完成登录。这个流程已经能用,也保留了现有的认证和审计机制。

但和 AI 一起排查问题时,还要不停地复制命令、切到终端执行,再把结果贴回来。于是有了一个很直接的想法:继续用原来的 SecureCRT,让 AI 也能操作我已经打开的那一个标签页。

这个小工具叫 SecureCRT Bridge。源码、构建方法和安装包放在 GitHub 仓库。

它用起来是什么样

安装后,仍然按原来的方式连接 SSH。新会话通过登录钩子自动接入本地桥接。AI 可以列出会话、读屏幕、执行命令,并获取输出和退出码;这些操作也会出现在我的终端窗口中。

窗口不需要保持前台,最小化也可以。多个标签页各自拥有独立的会话 ID,不同会话能并行执行任务。同一个标签页的 AI 写入则通过租约和任务状态串行处理。

安装器是原生 Windows 窗口,可以自动查找 SecureCRT,也能手动选目录。安装、升级、卸载共用一个入口,执行时显示进度条和具体步骤。日志会给出程序、skill、配置及备份的完整路径。

交付物是一个带内嵌 DLL 的 EXE。用户不需要安装 Rust、Python 或 Node;需要自己已有 Windows x64 和 SecureCRT x64 环境。

为什么选择接入现有会话

另一种办法是让 AI 直接启动自己的 SSH 客户端。但在堡垒机工作流里,连接可能依赖现有客户端交接的登录信息、临时会话或认证步骤。重复实现这条链路会增加维护成本,也改变了原来的操作方式。

桥接的边界很明确:连接和认证仍交给 SecureCRT 与堡垒机;桥接只操作已经获得授权的终端。它不提取远程密码,不建立第二条 SSH 连接,也不绕过认证。

这里依赖的是 SecureCRT 提供的脚本接口。Windows 上支持 ActiveX 脚本语言,脚本能访问终端对象;官方也提供了用 ReadString() 采集命令输出的示例。参见 Scripting FAQ 和 ReadString 示例。

三部分实现

AI / CLI
   │  带本机令牌的 HTTP 请求
   ▼
Rust Broker(127.0.0.1)
   ▲  轮询任务、报告屏幕和结果
   │
Rust Active Scripting COM 引擎
   │  SecureCRT 脚本 API
   ▼
已经登录的 SSH 标签页 ← 用户仍然可以直接操作

Broker 维护会话、租约、任务和结果。它只监听回环地址,所有接口都要求本机令牌。CLI 自动读取本机发现信息,不要求使用者手工复制令牌。

接入引擎 是 Rust 实现的 Active Scripting COM DLL,注册独立的 SecureCRTBridge.Native,不替换系统已有脚本引擎。SecureCRT 运行登录钩子时加载它,再通过宿主暴露的 crt 对象访问终端。

安装器 负责查找路径、备份并修改登录配置、写入 EXE 与 DLL、注册当前用户的 COM 组件,以及配置启动项和 skill。耗时操作放在工作线程,界面接收进度事件,因此日志能随着实际步骤更新。

安装记录保存每项配置的修改前后值和自有文件的哈希。卸载时,如果某项仍保持桥接安装时的值,就恢复原值;如果用户后来改过,就保留并提示。这样可以避免把安装之后的新配置一并覆盖掉。

多标签页的关键:绑定对象,而不是焦点

通过鼠标键盘模拟操作终端,很容易受当前焦点、弹窗和标签顺序影响。共享终端尤其需要避免这种不确定性。

接入时,桥接获取并保留 crt.GetScriptTab() 返回的实际标签页对象。此后的屏幕读取和命令发送都通过这个对象执行,不再查询“当前选中的标签页”。切换到另一个标签页不会改变命令目标,重新连接则会更换会话 ID。

COM 终端 API 调用保持在 SecureCRT 的宿主脚本线程中。Broker 通过轮询给各个接入实例派发任务,从而允许不同标签页独立工作。

会话标题只用于辅助辨认,不能充当唯一标识,更不能单凭标题证明服务器身份。AI 应结合会话 ID、屏幕内容和必要的只读命令确认目标。

如何判断一条命令结束

终端是连续字符流,没有天然的“这段就是某条命令的响应”。当前实现针对 POSIX shell,把命令包在两个随机标记之间:

(
  printf '\n%s\n' '__SCB_''request_BEGIN__'
  sh -c 'hostname; pwd'
  rc=$?
  printf '\n%s:%s\n' '__SCB_''request_END__' "$rc"
)

实际请求 ID 每次生成。发送给终端的标记被拆成相邻的引号字符串,而 shell 输出时会拼成完整标记。这样 ReadString() 就不会把终端对输入命令的回显误认成真正的开始或结束。

采集器等待开始标记,读取到结束标记,再解析退出码。命令在子 shell 中执行,因此 cd 或环境变量赋值不会改变用户父 shell 的上下文。需要目录上下文时,应一次提交 cd /path && command。

这种协议适用于空闲的 POSIX shell,不适用于密码提示、编辑器、数据库客户端或任意全屏交互程序。需要交互时可以发送原始输入,但不能沿用 shell 命令的结束判断。

三个在实测中发现的问题

1. 接收 socket 仍然是非阻塞的

最初让监听 socket 以非阻塞方式工作,但接受连接后,工作线程使用了阻塞式的 read_exact()。在测试的 Windows 环境中,接收 socket 继承了非阻塞状态,遇到分段到达的 HTTP 请求就可能提前失败。

修复是在接收后显式调用 set_nonblocking(false),再设置读写超时。回归测试故意将请求拆开发送,确保代码不会把暂时没有后续字节误当成连接结束。

2. 登录脚本与手工脚本的初始状态不同

早期版本出现过登录后终端一片空白。连接并不一定断了,而是登录脚本进入时 Screen.Synchronous 为真,常驻循环没有持续消费同步缓冲,收到的数据没及时显示到用户窗口。

隔离测试用独立配置和本机模拟登录复现了这个差异。处理方式是:空闲时明确关闭同步缓冲;捕获命令输出时才使用同步读取;正常返回和错误退出都恢复显示状态。由此也得到一个教训:单独运行脚本成功,不能证明它作为登录脚本常驻时也正确。

3. 短超时轮询会丢掉延时命令的前半段输出

最初每次只给 ReadString() 一秒,超时后再继续读。快速命令看起来没问题,碰到“先输出、等待几秒、再输出”的命令,API 结果却只剩后半段,而终端上能看到全文。

在所测试的 SecureCRT 版本中,读取超时会返回空字符串,已经消费的部分内容无法通过下一次调用补回来。改成一次调用覆盖命令的剩余时限后,开始与结束之间的输出才完整保留。

现在超时会标记为未知结果,不自动重发,也不自动发送 Ctrl+C。

超时不是“没执行”

这是桥接里比返回漂亮 JSON 更重要的一点:命令可能已经到达服务器,只是结果没有成功送回客户端。

每次写入在派发前生成请求 ID。相同 ID 和相同请求在当前服务生命周期内去重;客户端遇到错误后先查询这个 ID 的结果,不能直接再执行一次。未知结果会继续占用会话,只有确认终端已经空闲后才能解除。

这也不是跨重启的 exactly-once 保证。Broker 重启会丢失内存中的任务结果;“查不到 ID”不能证明“命令没有执行过”。

租约也有范围:它协调多个 AI 对话,不能锁住用户的物理键盘。人和 AI 仍应轮流操作同一个标签页。暂停会取消未派发任务,已在远端运行的命令则需要按实际情况处理。

让其他 AI 对话接入

安装器把 securecrt-bridge skill 放到当前用户的 Codex skills 目录;自定义 CODEX_HOME 时跟随该目录。

skill 写的是操作约定:先发现安装路径,再列会话、读屏幕、申请租约,执行结束后释放;明确超时查询、未知结果处理和暂停语义。它不保存服务器密码或本机令牌,也不授予新的系统权限。

手动使用 CLI 的流程类似:

& $scb sessions | Out-String
& $scb screen SESSION_ID | Out-String
& $scb claim SESSION_ID --owner my-task | Out-String
& $scb exec SESSION_ID --owner my-task --command 'hostname; pwd' | Out-String
& $scb release SESSION_ID --owner my-task | Out-String

$scb 指向安装后的 securecrt-bridge.exe,完整发现方式见仓库 README。

验证范围与当前限制

自动测试覆盖 HTTP 鉴权、分段请求、租约和重复派发防护、配置恢复、COM 生命周期、中文与退出码、延时输出和超时不重试。

真实 SecureCRT 9.7.1 测试中,两个最小化的 SSH 标签页分别执行了带四秒等待的只读命令,完整输出都返回,退出码均为 0。用 Broker 同一本机记录的派发与完成时间计算,两条任务的执行区间重叠约 4.5 秒。没有拿两台服务器各自的时钟来推断并行。

安装器增加了控件状态自检;它能验证进度事件、模式切换、防重复点击和错误后重试,但不能代替真实的完整安装/卸载验证。具体通过项目和未覆盖项持续记录在仓库的 验证记录。

当前只支持 Windows x64 与 SecureCRT x64,命令采集面向 POSIX shell。已有内置 Python 登录脚本会保留并跳过;第三方 ActiveX 脚本串联仍需更多兼容验证。显式 /SCRIPT 参数也可能覆盖默认登录钩子。

本机服务只绑定 127.0.0.1,接口要求令牌并拒绝带 Origin 的浏览器请求,但同一 Windows 用户下的恶意程序仍属于信任边界之内。连接发现文件不要上传到仓库。项目采用 MIT 许可,不包含 SecureCRT 本体或堡垒机客户端。

对我来说,它最有价值的地方,是把已经能用的终端接进了 AI 的工作流程,同时保留同一个窗口里的可见操作。后续需要继续打磨的,是更多登录脚本和 SecureCRT 版本的兼容性,以及更完整的安装与卸载验收。