Extensions 扩展入门
本教程共 30 篇 · 第 16 篇 · 更新于 2026-08-10 · 约 8 分钟阅读
本节目标:搞清楚 Extension 是什么、它和 Skill 的本质区别,掌握扩展的加载位置与文件结构,能写出第一个可运行的 Hello World 扩展。
到这一章为止,你已经能用 pi 写代码、配模型、用 Skill 定制行为。但如果想深入一步——给 pi 加一个它原本没有的工具、拦截每次工具调用做安全检查、在 AI 回复前后插入自定义逻辑——这些 Skill 都做不到。
Skill 是”教 AI 怎么想”。Extension 是”让 pi 怎么做”。
Extension 和 Skill 到底差在哪
这两个概念容易搞混,一句话说清:
| Skill | Extension | |
|---|---|---|
| 本质 | 提示词级别 | 代码级别 |
| 写的什么 | Markdown 文件(告诉 AI 规则和流程) | TypeScript 文件(注册工具、监听事件) |
| 能力边界 | 只能影响 AI 的思考方向 | 可以改 pi 的运行行为 |
| 能不能加新工具 | ❌ | ✅ |
| 能不能拦截操作 | ❌ | ✅ |
| 能不能改 UI 渲染 | ❌ | ✅ |
类比一下:Skill 像给 AI 写了一份”工作手册”,告诉它碰到什么情况怎么想、按什么步骤来。Extension 像给 pi 装了一个”硬件模块”,直接增加它原来没有的功能。
两个机制不互斥。实际项目里,Extension 负责”有没有这个能力”,Skill 负责”用这个能力时按什么规矩来”。
Extension 能做哪些事
一个 Extension 的入口是一个 TypeScript 工厂函数,拿到 ExtensionAPI 实例后,你可以做这些:
- 注册自定义工具:
pi.registerTool()—— AI 能调用的新功能,比如查数据库、调内部 API - 监听生命周期事件:
pi.on()—— 在 agent 启动、工具调用、会话切换等节点插入逻辑 - 注册自定义命令:
pi.registerCommand()—— 用户打/mycommand就能调 - 修改 LLM 上下文:在消息发给模型之前动手脚
- 渲染自定义 UI:
ctx.ui系列方法,做终端内的选择框、确认弹窗、通知条 - 覆盖内置工具:注册同名工具替换
read、bash、edit等默认行为
NoteExtension 以你当前用户的系统权限运行,能做任何事。只装来源可信的扩展。
扩展从哪加载
pi 会在几个固定位置自动发现扩展:
| 位置 | 作用范围 | 说明 |
|---|---|---|
~/.pi/agent/extensions/*.ts | 全局 | 对所有项目生效 |
~/.pi/agent/extensions/*/index.ts | 全局 | 子目录形式的扩展 |
.pi/extensions/*.ts | 项目 | 仅当前项目,需信任 |
.pi/extensions/*/index.ts | 项目 | 项目子目录扩展 |
自动发现的扩展可以用 /reload 热重载,不用重启 pi。
临时测试不想放目录的话,用 CLI 参数直接加载:
pi -e ./my-extension.ts
也可以在 settings.json 里指定路径:
{
"extensions": [
"/path/to/local/extension.ts",
"/path/to/local/extension-dir"
]
}
扩展的文件结构
三种常见组织形式,按复杂度递增。
单文件扩展
最简形式,一个 .ts 文件就够了,适合几十行的小工具:
~/.pi/agent/extensions/
└── my-extension.ts
目录扩展
包含 index.ts 作为入口,适合拆成多个模块:
~/.pi/agent/extensions/
└── my-extension/
├── index.ts # 入口,导出一个默认函数
├── tools.ts # 工具实现
└── utils.ts # 辅助函数
带 npm 依赖的扩展
需要第三方包时,加 package.json 然后 npm install:
~/.pi/agent/extensions/
└── my-extension/
├── package.json
├── node_modules/
└── index.ts # 入口必须放在扩展根目录,自动发现规则只匹配 */index.ts
pi 用 jiti 加载 TypeScript,不需要编译。如果工厂函数返回 Promise,pi 会等它 resolve 再继续启动。
Tip开发时放在
~/.pi/agent/extensions/目录里,改了代码/reload一下就生效。pi -e只适合快速验证。
可用的内置 Import
写扩展时可以直接 import 这些 pi 自带包,不需要额外安装:
| 包名 | 用途 |
|---|---|
@earendil-works/pi-coding-agent | 扩展类型:ExtensionAPI、ExtensionContext、事件类型 |
typebox | 定义工具参数的 Schema |
@earendil-works/pi-ai | AI 工具,如 StringEnum(Google API 兼容的枚举) |
@earendil-works/pi-tui | TUI 组件,用于自定义终端渲染 |
这些应该列为 peerDependencies,不要打进你的扩展包里。
第一个扩展:从零到跑起来
下面写一个能实际跑起来的扩展。它做三件事:
- 会话启动时弹一条通知
- 注册一个
greet工具,让 AI 能跟人打招呼 - 注册一个
/hello命令,用户直接敲就能响应
在 ~/.pi/agent/extensions/ 下创建 my-extension.ts(如果目录不存在先建目录):
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// 1. 会话启动时通知
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("我的扩展已加载!", "info");
});
// 2. 注册 greet 工具——AI 可以调用
pi.registerTool({
name: "greet",
label: "打招呼",
description: "向指定的人打招呼,返回问候语",
parameters: Type.Object({
name: Type.String({ description: "要打招呼的人名" }),
}),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `你好,${params.name}!` }],
details: {},
};
},
});
// 3. 注册 /hello 命令——用户直接敲
pi.registerCommand("hello", {
description: "向世界问好",
handler: async (args, ctx) => {
ctx.ui.notify(`你好 ${args || "世界"}!`, "info");
},
});
}
测试它:
pi -e ~/.pi/agent/extensions/my-extension.ts
进去之后试试:
- 直接敲
/hello,看终端会不会弹出通知 - 对 AI 说”用 greet 工具跟小明打个招呼”,看 AI 会不会调用你的工具
Tip放在
~/.pi/agent/extensions/目录里的扩展会被自动发现。上面的-e参数只是临时加载方式——开发完后把文件放对位置,以后启动 pi 就会自动加载。
开发时要注意什么
几条从踩坑里总结的经验:
- 不要在工厂函数里启动后台资源(进程、socket、文件监听、定时器)。工厂函数只是注册,把资源启动延迟到
session_start事件里,在session_shutdown里清理。 - 改了扩展代码用
/reload热重载,不用退出 pi 再进。 tool_call事件里event.input可以直接改,你改完的参数就会传给工具执行。
这一章建立了 Extension 的基础认知。下一章深入自定义工具——怎么定义一个参数完备、错误处理到位、能跑在生产环境里的工具。