第 04 模块 · 1 节

理解扩展与事件系统

《Pi 进阶实战》04 扩展开发 Extensions · 本节时长 32 分钟

贯穿项目技能和模板是「配置」,扩展是「真正的程序」。这一节理解扩展是什么、事件系统怎么运作——为把 md-tools 变成 Pi 的一个「原生能力」打基础。你要能看懂一份扩展代码干了什么。

扩展是什么

官方一句话:扩展(Extension)是 TypeScript 模块,用来扩展 Pi 的行为。 它们能订阅生命周期事件、注册模型可调用的自定义工具、添加命令,等等。

它能力很广,官方列了一串关键能力:

  • 自定义工具:用 pi.registerTool() 注册
  • 事件拦截:阻塞/修改工具调用、注入上下文、自定义压缩
  • 用户交互:用 ctx.ui 提示用户(select / confirm / input / notify)
  • 自定义 UI:完整的 TUI 组件
  • 自定义命令:注册 /mycommand
  • 会话持久化:用 pi.appendEntry() 保存跨重启的状态
  • 自定义渲染:控制工具调用/结果在 TUI 里的显示

一句话:扩展是给 Pi 写程序,不是给 Pi 写配置。

贯穿项目最终我们要写一个扩展,把 md-tools 的检查能力注册成 Pi 的工具和 /md 命令,还能在用户执行危险操作(比如 rename)时拦截确认。这就是「工具化」。

一份最小扩展长什么样

官方给了一个非常清晰的完整示例,涵盖了事件、工具、命令三件事:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

export default function (pi: ExtensionAPI) {
  // 订阅事件
  pi.on("session_start", async (_event, ctx) => {
    ctx.ui.notify("Extension loaded!", "info");
  });

  // 注册自定义工具
  pi.registerTool({
    name: "greet",
    label: "Greet",
    description: "Greet someone by name",
    parameters: Type.Object({
      name: Type.String({ description: "Name to greet" }),
    }),
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      return {
        content: [{ type: "text", text: `Hello, ${params.name}!` }],
        details: {},
      };
    },
  });

  // 注册命令
  pi.registerCommand("hello", {
    description: "Say hello",
    handler: async (args, ctx) => {
      ctx.ui.notify(`Hello ${args || "world"}!`, "info");
    },
  });
}

注意核心三件事:pi.on 订阅事件、pi.registerTool 注册工具、pi.registerCommand 注册命令。一个扩展就是这三个动作的组合。


事件系统:Pi 运行到哪,你就能在哪插一脚

扩展最强大的部分就是事件系统。官方画了一整张生命周期图,几个关键节点:

事件 时机
project_trust 信任决定前
session_start 会话启动/加载
input 收到用户输入后(可拦截/改写)
before_agent_start 提交提示词后、agent 循环前
tool_call 工具执行前(可阻塞)
tool_result 工具执行后(可修改结果)
message_end 消息最终确定时
session_shutdown 会话销毁前

一句话:从启动到退出,每个环节都有一扇「事件门」,扩展可以在门口做拦截、修改、注入。


你能用事件做什么

官方给的示例场景很实用:

  • 权限门rm -rfsudo 前先确认
  • Git 检查点:每个回合 stash,切分支恢复
  • 路径保护:阻止写 .envnode_modules/
  • 自定义压缩:用你自己的方式总结对话
  • 有状态工具:待办列表、连接池

md-tools 最贴切的是权限门——在 tool_call 事件里,遇到 rename 或危险操作就弹出确认。


落地练习:读懂并试跑一份扩展

  1. 新建 ~/.pi/agent/extensions/my-first.ts,把上面那份最小扩展完整复制进去
  2. pi -e ./my-first.ts 或放进自动发现目录后启动
  3. 观察:启动时有 notify,输入 /hello 有回应,让模型调用 greet 工具

怎么判断做对了?——启动提示扩展已加载,/hello 能触发通知,greet 工具能被模型调用。

卡住了怎么办? 没反应 → 确认放进了被扫描目录或用 -e 指定了路径。语法错 → 检查 TypeScript 是否有报错。工具不出现 → 确认 registerTool 已调用、参数定义正确。


常见坑:在扩展里放「跑不完的后台任务」

官方专门提醒过:扩展工厂可能在根本不启动会话的调用里运行。不要在工厂里直接启动后台资源(进程、socket、文件监听、定时器)。

正确做法:把后台资源延迟到 session_start 或真正需要它的命令/工具/事件里再启动,并在 session_shutdown 里清理。否则你开的资源可能一直挂在那,甚至拖垮整个进程。


小结

  1. 扩展 = 写 TypeScript 程序,能订阅事件、注册工具、注册命令
  2. 核心三动作:pi.onpi.registerToolpi.registerCommand
  3. 事件系统覆盖全生命周期,每个环节都能拦截/修改/注入
  4. 典型场景:权限门、Git 检查点、路径保护、有状态工具
  5. 别在工厂里启动后台资源,延迟到需要时并记得清理

下一节,正式动手——注册自定义工具和斜杠命令。