第 10 章 · Extensions 入门:TypeScript 扩展 API
本章目标:理解扩展能做什么、放在哪里、如何加载与热重载,掌握扩展的生命周期事件全景,并完整走读一个最小扩展示例。
10.1 扩展是 pi 的"超能力层"
如果说技能是"给模型看的说明书",扩展就是"给 pi 本体动的手术"。扩展是 TypeScript 模块,可以:
- 注册自定义工具——
pi.registerTool(),让 LLM 能调用你的函数; - 拦截事件——阻止或修改工具调用(如
rm -rf前弹窗确认)、注入上下文、自定义压缩; - 用户交互——通过
ctx.ui弹出 select/confirm/input/notify; - 自定义 UI 组件——
ctx.ui.custom()构建带键盘交互的 TUI 组件; - 自定义命令——
pi.registerCommand()注册/mycommand; - 会话持久化——
pi.appendEntry()存储跨重启的状态。
官方列举的典型用例:危险命令权限门、git 自动 checkpoint、保护 .env 不被写入、按自己方式做压缩、文件监听/webhook 集成……甚至有人写了贪吃蛇在等待时玩。pi 刻意不内置子代理和计划模式,就是要靠这个生态来长。
10.2 放在哪里、怎么加载
~/.pi/agent/extensions/*.ts ← 全局自动发现
~/.pi/agent/extensions/*/index.ts ← 全局(子目录形式)
.pi/extensions/*.ts ← 项目级(受 Project Trust 门控)
.pi/extensions/*/index.ts ← 项目级(子目录形式)补充通道:settings.json 的 extensions 数组可指定本地路径/目录;packages 数组可加载 npm/git 分发的包;CLI 用 -e ./path.ts 临时挂载。
开发期用 -e,稳定后进自动发现目录
官方建议:只有放进自动发现位置的扩展才能配合 /reload 热重载;pi -e ./x.ts 只适合快速试验。
# 快速测试一个扩展
pi -e ./my-extension.ts
# 放入自动发现位置后,改代码后在交互模式里:
/reload权限即用户权限
扩展以你的完整系统权限运行、可执行任意代码——项目级扩展之所以被 Project Trust 门控就是这个原因。只安装你信任的来源。
10.3 最小扩展示例走读
下面这个官方风格的示例覆盖了四类核心能力,逐段拆解:
// ~/.pi/agent/extensions/my-extension.ts
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");
});
// ② 拦截工具调用:危险 bash 命令先问过用户
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
if (!ok) return { block: true, reason: "Blocked by user" };
}
});
// ③ 注册自定义工具:LLM 可直接调用的 greet
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: {},
};
},
});
// ④ 注册自定义命令 /hello
pi.registerCommand("hello", {
description: "Say hello",
handler: async (args, ctx) => {
ctx.ui.notify(`Hello ${args || "world"}!`, "info");
},
});
}四个关键点:
- 默认导出一个工厂函数,参数是
ExtensionAPI;工厂可以是异步的,pi 会等它完成再继续启动; - 工具参数 schema 用 typebox 的
Type.Object(...)描述,description字段会成为模型理解参数的依据; tool_call处理器返回{ block: true, reason }即可拦截本次工具执行;- 扩展经 jiti 加载,TypeScript 直接写、无需编译步骤。
依赖管理:在扩展旁放 package.json 并 npm install 后,node_modules 里的包自动可用;Node 内置模块(node:fs 等)天然可用。
10.4 生命周期事件全景
官方文档给出的启动到响应的完整事件流(节选主干):
pi 启动
├─ project_trust (仅用户/全局/CLI 扩展收到,早于项目资源加载)
├─ session_start { reason: "startup" }
└─ resources_discover
用户发送提示
├─ input (可拦截/改写/接管输入)
├─ before_agent_start (可注入消息、修改 system prompt)
├─ agent_start
│ ┌─ turn 循环(LLM 调工具则重复)─────────┐
│ ├─ turn_start → context(可改消息列表)
│ ├─ tool_execution_start
│ ├─ tool_call (可 block)
│ ├─ tool_result (可修改结果)
│ └─ turn_end
├─ agent_end
└─ agent_settled (无重试/压缩/后续消息时)
/new 或 /resume 切换会话
└─ session_before_switch → session_shutdown → session_start{reason}两个对初学者最重要的实践规则:
- 不要在工厂函数里启动后台资源(进程、socket、定时器)——工厂可能在没有会话的调用中运行;把这类初始化推迟到
session_start,并在session_shutdown里幂等地清理; project_trust早于项目资源:需要感知信任状态的逻辑应挂在 user/global 扩展上。
异步工厂的一个真实用途——启动时从本地服务拉取模型列表并注册 provider:
export default async function (pi: ExtensionAPI) {
const res = await fetch("http://localhost:1234/v1/models");
const payload = await res.json();
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "$LOCAL_OPENAI_API_KEY",
api: "openai-completions",
models: payload.data.map((m) => ({
id: m.id,
name: m.name ?? m.id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: m.context_window ?? 128000,
maxTokens: m.max_tokens ?? 4096,
})),
});
}调试方法:ctx.ui.notify() 打点最简单;类型定义可直接查看安装目录 node_modules/@earendil-works/pi-coding-agent/dist/。
本章小结
- 扩展 = 默认导出工厂函数的 TypeScript 模块,jiti 直载免编译;
- 自动发现位置:全局
~/.pi/agent/extensions/、项目.pi/extensions/(后者需信任),开发期用-e+/reload; - 四大能力:事件订阅(
pi.on)、工具注册(registerTool+ typebox)、命令注册(registerCommand)、UI 交互(ctx.ui); tool_call返回{block:true, reason}可拦截危险操作;- 后台资源延迟到
session_start再启动,session_shutdown幂等清理。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 扩展文件的正确导出形式是什么?
2. 想在 LLM 调用 bash 执行 rm -rf 前弹出确认框,应该订阅哪个事件?
3. 关于扩展的加载与热重载,正确的是?
4. 扩展需要在会话期间维护一个文件监听器,正确的做法是?
🛠️ 动手实践
- 实现本章的 my-extension.ts,验证
/hello命令、greet工具与rm -rf拦截三条路径都工作。 - 写一个
tool_result事件的处理器,把所有 bash 输出末尾追加一行[inspected by ext],观察对模型的影响。 - 故意在工厂函数里
setTimeout一个无限循环打印,再用规范写法(挪到 session_start + session_shutdown 清理)对比两种行为的差异。
完成练习后,进入下一章:Extensions 进阶。