Pi 插件解剖|ssh.ts:只用 221 行,让 Agent 直接在远程机器干活

一句话定位

通过 --ssh user@host 把 pi 的内置工具(read/write/edit/bash)全部切换到远程机器执行------221 行,工具改写(依赖注入换执行环境)的完整范例。

作用(为什么存在)

前三期拆过拦截、生命周期、行为替换,这期是工具改写:让 Agent 直接操作远程机器,而不是只在本地干活。

它的核心机制非常优雅:内置工具工厂接受 operations 参数 ------createReadTool(localCwd, { operations }) 替换底层实现,工具定义(参数、描述、渲染)完全不变。pi 只依赖输入输出契约,不关心真实实现是本地还是 SSH。

更妙的是一个插件同时支持两种模式--ssh 传了就远程执行,不传就完全走本地------插件对普通用户是透明的。

关键信息

内容
源码位置 examples/extensions/ssh.ts(221 行)
核心 API registerFlag/getFlag + registerTool(同名覆盖内置)+ createReadTool 等(注入 operations)+ on("user_bash") + on("before_agent_start")
插件类型 工具改写型(依赖注入换执行环境)

触发流程 / 数据流

lua 复制代码
pi -e ssh.ts --ssh user@host
  → 工厂期:registerFlag("ssh") 注册 --ssh;创建本地工具实例;同名覆盖 read/write/edit/bash
  → session_start:getFlag("ssh") 解析远程地址
      ├─ 含 :path → 直接用
      └─ 无 path → 远程执行 pwd 自动探测
  → 工具 execute:getSsh() 有值 → 用远程 ops 重建工具 → 远程执行;无值 → 本地
  → user_bash(! 命令):SSH 激活时返回远程 bash ops
  → before_agent_start:改 system prompt 的 cwd,让 LLM 认知工作在远程

架构 / 流程

关键代码解读

typescript 复制代码
// ① operations 注入:替换工具底层实现(ssh.ts:47-62)
function createRemoteReadOps(remote, remoteCwd, localCwd): ReadOperations {
  const toRemote = (p) => p.replace(localCwd, remoteCwd);  // 本地路径 → 远程路径
  return {
    readFile: (p) => sshExec(remote, `cat ${JSON.stringify(toRemote(p))}`),
    access:   (p) => sshExec(remote, `test -r ${JSON.stringify(toRemote(p))}`).then(() => {}),
    detectImageMimeType: async (p) => { /* file --mime-type ... */ },
  };
}

// ② 同名覆盖内置工具:SSH 激活时换远程实现(ssh.ts:128-140)
pi.registerTool({
  ...localRead,   // 定义(name/description/parameters)不变
  async execute(id, params, signal, onUpdate, _ctx) {
    const ssh = getSsh();
    if (ssh) {
      const tool = createReadTool(localCwd, { operations: createRemoteReadOps(ssh.remote, ssh.remoteCwd, localCwd) });
      return tool.execute(id, params, signal, onUpdate);  // 换成远程执行
    }
    return localRead.execute(id, params, signal, onUpdate);  // 默认本地
  },
});

// ③ session_start 延迟解析:工厂期 CLI flag 不可用(ssh.ts:184-199)
pi.on("session_start", async (_event, ctx) => {
  const arg = pi.getFlag("ssh") as string | undefined;  // 此时 flag 才可读
  if (arg) {
    if (arg.includes(":")) {
      const [remote, path] = arg.split(":");
      resolvedSsh = { remote, remoteCwd: path };
    } else {
      const pwd = (await sshExec(arg, "pwd")).toString().trim();  // 无路径→远程 pwd 探测
      resolvedSsh = { remote: arg, remoteCwd: pwd };
    }
    ctx.ui.setStatus("ssh", ctx.ui.theme.fg("accent", `SSH: ${resolvedSsh.remote}:${resolvedSsh.remoteCwd}`));
  }
});

亮点 / 踩坑

亮点 1:依赖注入换执行环境。 operations 是接口契约(Read/Write/Edit/Bash 四个接口各不同),注入实现即可换环境------工具定义、LLM 感知、渲染全都不变。这是「面向接口编程」在插件层的落地。

亮点 2:生命周期分离。 注册动作(registerFlag/registerTool)在工厂期做,读运行时状态(getFlag)必须等 session_start------因为工厂期 CLI flag 还没注入。一个 resolvedSsh = null 占位 + getSsh() 现查,让本地/远程两种模式同时成立。

踩坑:只支持 key 认证。 源码注释明确「SSH key-based auth (no password prompts)」,且 spawn("ssh", ..., { stdio: ["ignore", "pipe", "pipe"] }) 把 stdin 设成 ignore------远程要密码时没法输入,只能等超时 。现象:每次工具调用卡十几秒到几十秒(17s/53s),全是等密码超时。解法:配置 SSH 免密(ssh-keygen + ssh-copy-id + 远程 chmod 700 ~/.ssh && chmod 600 authorized_keys)。诊断口诀:先直连 time ssh host "echo ok" 排除网络,再查认证方式

边界(Limitations)

边界 表现
认证 必须 SSH key 免密(stdin ignore 无法输密码)
生效时机 --ssh 在 session_start 才解析;无 flag 时完全走本地
路径映射 toRemotelocalCwd → remoteCwd 字符串替换,路径结构不同会错
覆盖范围 read/write/edit/bash 四个内置工具 + 用户 ! 命令

场景(Scenarios)

  • SSH 模式pi -e ssh.ts --ssh user@hostuser@host:/path → 所有工具远程执行,状态栏显示 SSH
  • 本地模式:无 --ssh → 完全等同内置工具(插件透明)
  • 无路径--ssh user@host → session_start 时远程 pwd 探测
  • 失败:认证失败 / 远程无 bash → sshExec reject → 工具报错

可借鉴的模式

  1. 依赖注入换执行环境 :工具工厂接受 operations 替换底层实现------「定义不变、实现可换」。pi 只依赖输入输出契约。
  2. 同名覆盖内置工具:registerTool 注册与内置同名工具即可接管行为------LLM 感知的工具接口完全不变。
  3. 工厂期/运行期分离:注册动作工厂期做,读运行时状态 session_start 才做------生命周期决定 API 可用阶段。
  4. JSON.stringify 防注入:命令拼接时把路径序列化成带引号的合法字面量,防止空格/特殊字符破坏 shell(也是命令注入防护)。
  5. system prompt 改写:before_agent_start 改 cwd,让 LLM 的认知与实际执行环境一致。

一句话总结

221 行教你「工具改写」:依赖注入换执行环境、同名覆盖接管内置工具、工厂期/运行期分离------一个插件同时支持本地和远程两种模式。

相关推荐
ITxiaobing20234 分钟前
广告归因场景下的IP情报工程化:提升AppsFlyer P360匹配精度的实践思路
大数据·人工智能·tcp/ip
mayaairi29 分钟前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
小艾.pino31 分钟前
MiniMax M3顶住新一代多模态大模型的架构与实战
人工智能·架构
DO_Community36 分钟前
GPT 6 Astra 已上线 DigitalOcean AI 推理云:AGI 时代的计算机操作模型来了
人工智能·gpt·agi
字节跳动视频云技术团队43 分钟前
火山引擎 AI MediaKit X 懂车帝,探索汽车内容智能创作新方式
人工智能·音视频开发
MindUp1 小时前
大模型技术在股票分析场景的应用与工具调研
人工智能·金融
tuanxiang1 小时前
在线AI检测接口误判问题排查与绕过实践
人工智能
suaizai_1 小时前
AI进化:从“回答问题”到“完成任务”
人工智能
广凌股份(广凌科技)1 小时前
2026年高校采购管理系统选型指南 | 5款软件深度测评
大数据·人工智能
加密社1 小时前
GPT-6 Astra 100 Studies | 100个AI生成的HTML5视觉作品集
人工智能·gpt