13  扩展开发

13.1 扩展系统概述

扩展(Extension)是 Pi 最强大的定制机制。扩展是 TypeScript 模块,可以订阅生命周期事件、注册供 LLM 调用的自定义工具、添加斜杠命令、渲染自定义 UI 组件,几乎能覆盖 Pi 的每一个行为层面。

Pi 的核心设计理念是”保持内核极小,通过扩展实现无限可能”。这意味着你在日常使用中遇到的大部分功能,都可以通过扩展来定制或增强。

13.1.1 核心能力一览

能力 说明
自定义工具 通过 pi.registerTool() 注册 LLM 可调用的工具
事件拦截 拦截或修改工具调用、注入上下文、自定义压缩
用户交互 通过 ctx.ui(select、confirm、input、notify)与用户交互
自定义 UI 通过 ctx.ui.custom() 实现完整的 TUI 组件
自定义命令 通过 pi.registerCommand() 注册 /mycommand 等命令
会话持久化 通过 pi.appendEntry() 存储跨重启的状态
自定义渲染 控制工具调用结果和消息在 TUI 中的显示方式

13.2 快速开始

创建文件 ~/.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");
  });

  // 拦截危险命令
  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" };
    }
  });

  // 注册自定义工具
  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");
    },
  });
}

使用 -e 标志测试:

pi -e ./my-extension.ts
Tip

让 Pi 帮你创建扩展!直接在对话中说”帮我创建一个扩展,实现 XXX 功能”,Pi 会自动生成代码。

13.3 扩展位置

扩展按以下优先级自动发现:

位置 作用域 说明
~/.pi/agent/extensions/ 全局 所有项目共享,自动发现
.pi/extensions/ 项目级 仅当前项目,需项目受信任后加载
-e ./path.ts CLI 仅本次运行,适合快速测试
Warning

安全提示:扩展拥有完整的系统权限,可以执行任意代码。只安装来自可信来源的扩展。

放在自动发现位置的扩展可以通过 /reload 命令热重载,无需重启 Pi。

13.4 可用导入

扩展模块可以通过相对路径或 bare import 引入依赖:

// 从 Pi 核心包导入类型
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

// 从 typebox 导入(用于参数 schema 定义)
import { Type } from "typebox";

// 从 Pi TUI 包导入组件
import { Text, Box, Container } from "@earendil-works/pi-tui";

// 从 Pi AI 包导入
import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";

如果扩展需要第三方依赖,在扩展目录创建 package.json

{
  "name": "my-extension",
  "dependencies": {
    "zod": "^3.0.0",
    "chalk": "^5.0.0"
  },
  "pi": {
    "extensions": ["./src/index.ts"]
  }
}

运行 npm install 后即可在扩展中导入 node_modules/ 中的包。

13.5 编写扩展

13.5.1 默认导出函数

最基本的形式——导出一个接收 ExtensionAPI 的函数:

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

export default function (pi: ExtensionAPI) {
  // 注册事件监听器、工具、命令等
  pi.on("session_start", async (_event, ctx) => {
    ctx.ui.notify("Hello from my extension!", "info");
  });
}

13.5.2 异步工厂函数

当需要在启动时执行异步操作(如从远程 API 获取配置)时,导出 async 函数:

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

export default async function (pi: ExtensionAPI) {
  // 启动时拉取配置
  const config = await fetch("https://api.example.com/config").then(r => r.json());

  // 根据配置注册不同的工具
  if (config.enableSearch) {
    pi.registerTool({
      name: "search",
      // ...
    });
  }
}
Note

Pi 会等待异步工厂函数完成后再继续启动流程,因此在此注册的提供者和工具在交互式启动时就可使用。

13.5.3 长生命周期资源与关闭

对于需要清理的扩展(如文件监听、数据库连接、WebSocket),在 session_shutdown 事件中释放资源:

export default function (pi: ExtensionAPI) {
  let watcher = startFileWatcher();

  pi.on("session_shutdown", async () => {
    watcher.close();
    watcher = null;
  });

  pi.on("session_start", async () => {
    if (!watcher) watcher = startFileWatcher();
  });
}

13.6 扩展风格

根据复杂度,扩展可以采用不同的组织方式:

  1. 单文件扩展(简单场景):所有逻辑在一个 .ts 文件中
  2. 多文件扩展(中等复杂度):主文件导入辅助模块
  3. 包形式扩展(复杂场景):完整的 npm 包,包含 package.json 和依赖管理

13.7 事件系统

13.7.1 生命周期概览

graph TD
    A[pi 启动] --> B[project_trust]
    B --> C[session_start]
    C --> D[resources_discover]
    D --> E[用户发送消息]
    E --> F{是扩展命令?}
    F -->|是| G[执行扩展命令]
    F -->|否| H[input 事件]
    H --> I[before_agent_start]
    I --> J[agent_start]
    J --> K[turn 循环]
    K --> L[turn_start]
    L --> M[context 事件]
    M --> N[provider 请求/响应]
    N --> O{LLM 调用工具?}
    O -->|是| P[tool_execution_start]
    P --> Q[tool_call 事件]
    Q --> R[tool_result 事件]
    R --> S[tool_execution_end]
    S --> K
    O -->|否| T[turn_end]
    T --> U[agent_end]
    U --> V[agent_settled]

13.7.2 启动事件

13.7.2.1 project_trust

在决定是否信任项目之前触发。仅全局扩展和 CLI 扩展参与:

pi.on("project_trust", async (event, ctx) => {
  // event.cwd - 当前工作目录
  if (await ctx.ui.confirm("Trust project?", event.cwd)) {
    return { trusted: "yes", remember: true };
  }
  return { trusted: "undecided" };
});

13.7.2.2 session_start

会话启动、加载或重载时触发:

pi.on("session_start", async (event, ctx) => {
  // event.reason: "startup" | "reload" | "new" | "resume" | "fork"
  // event.previousSessionFile: 切换前的会话文件路径
  ctx.ui.notify(`Session started: ${event.reason}`, "info");
});

13.7.2.3 resources_discover

会话启动后触发,允许扩展贡献额外的 skill、prompt 和 theme 路径:

pi.on("resources_discover", async (event) => {
  return {
    skillPaths: ["/path/to/skills"],
    promptPaths: ["/path/to/prompts"],
    themePaths: ["/path/to/themes"],
  };
});

13.7.3 会话事件

事件 触发时机
session_start 会话启动/加载/重载
session_info_changed 会话名称变更
session_before_switch 新建/切换会话之前
session_before_fork 分支/克隆之前
session_before_compact 压缩之前
session_compact 压缩执行
session_before_tree 树状导航之前
session_tree 树状导航执行
session_shutdown 会话关闭/清理

13.7.4 Agent 事件

事件 触发时机
before_agent_start Agent 开始处理之前(可注入消息或修改系统提示)
agent_start Agent 开始处理
agent_end Agent 处理完成
agent_settled 无更多重试/压缩/跟进时

13.7.5 模型事件

事件 触发时机
context 每轮对话前(可修改消息列表)
before_provider_headers 发送请求前(可修改 headers)
before_provider_request 发送请求前(可检查或替换 payload)
after_provider_response 收到响应后(状态码和 headers)
model_select 模型切换时
thinking_level_select 思考级别变更时

13.7.6 工具事件

事件 触发时机
tool_execution_start 工具开始执行
tool_call 工具被调用(可阻止)
tool_execution_update 工具执行中的流式更新
tool_result 工具返回结果(可修改)
tool_execution_end 工具执行结束

13.7.7 用户 Bash 事件

事件 触发时机
bash_input 用户在 bash 模式下输入命令
bash_execution_update bash 命令执行中的流式输出
bash_end bash 命令执行结束

13.7.8 输入事件

13.7.8.1 input

在用户输入处理之前触发,可以拦截、转换或完全处理输入:

pi.on("input", async (event, ctx) => {
  // event.text - 用户输入的原始文本
  // 返回 { handled: true } 可阻止默认处理
  if (event.text === "/secret") {
    ctx.ui.notify("Secret command!", "info");
    return { handled: true };
  }
  // 返回 { text: "modified" } 可转换输入
});

13.8 ExtensionContext API

每个事件处理函数接收一个 ExtensionContext(简称 ctx),提供与 Pi 交互的能力:

13.8.1 UI 操作

// 通知
ctx.ui.notify("Message", "info"); // "info" | "warning" | "error"

// 确认对话框
const ok = await ctx.ui.confirm("Title", "Message");

// 文本输入
const text = await ctx.ui.input("Enter name:");

// 选择列表
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);

// 自定义 TUI 组件
const result = await ctx.ui.custom((tui, theme, keybindings, done) => {
  return new MyComponent({ onClose: done });
});

13.8.2 运行时信息

属性 说明
ctx.mode 当前运行模式
ctx.hasUI 是否有 TUI 界面
ctx.cwd 当前工作目录
ctx.isProjectTrusted() 项目是否已受信任
ctx.sessionManager 会话管理器实例
ctx.model 当前使用的模型
ctx.thinkingLevel 当前思考级别
ctx.signal AbortSignal,用于取消操作

13.8.3 会话控制

// 检查是否空闲
ctx.isIdle();

// 中止当前操作
ctx.abort();

// 获取上下文使用量
ctx.getContextUsage();

// 手动触发压缩
ctx.compact();

// 获取系统提示
ctx.getSystemPrompt();

13.9 ExtensionAPI 方法

13.9.1 事件注册

pi.on("event_name", async (event, ctx) => {
  // 处理事件
});

13.9.2 工具注册

pi.registerTool({
  name: "my_tool",
  label: "My Tool",
  description: "Does something useful",
  parameters: Type.Object({
    input: Type.String({ description: "Input parameter" }),
  }),
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    return {
      content: [{ type: "text", text: "Result" }],
      details: {},
    };
  },
});

13.9.3 消息发送

// 发送消息(触发 LLM 响应)
pi.sendMessage("Let's continue working");

// 以用户身份发送消息
pi.sendUserMessage("Please fix the bug in main.ts");

13.9.4 命令注册

pi.registerCommand("deploy", {
  description: "Deploy the current project",
  handler: async (args, ctx) => {
    ctx.ui.notify(`Deploying ${args}...`, "info");
    // 执行部署逻辑
  },
});

13.9.5 模型控制

// 切换模型
pi.setModel({ provider: "anthropic", id: "claude-sonnet-4-5" });

// 设置思考级别
pi.setThinkingLevel("high");

// 获取活跃工具列表
const tools = pi.getActiveTools();

// 设置活跃工具
pi.setActiveTools(["read", "write", "bash", "my_tool"]);

13.9.6 提供者注册

// 注册自定义提供者
pi.registerProvider("my-provider", {
  baseUrl: "https://api.example.com/v1",
  apiKey: "$MY_API_KEY",
  api: "openai-completions",
  models: [
    { id: "my-model", name: "My Model" },
  ],
});

// 注销提供者
pi.unregisterProvider("my-provider");

13.10 状态管理

扩展可以在会话中存储持久化状态。通过 pi.appendEntry() 创建自定义条目,状态会随会话文件保存:

pi.on("tool_execution_end", async (event, ctx) => {
  if (event.toolName === "bash") {
    // 记录每次 bash 执行
    pi.appendEntry("bash_log", {
      timestamp: Date.now(),
      command: event.args?.command,
      success: !event.isError,
    });
  }
});

13.11 自定义工具

13.11.1 工具定义

完整的工具定义包含以下字段:

pi.registerTool({
  name: "fetch_data",
  label: "Fetch Data",
  description: "Fetch data from a URL",
  parameters: Type.Object({
    url: Type.String({ description: "URL to fetch" }),
    method: Type.Optional(Type.Union([
      Type.Literal("GET"),
      Type.Literal("POST"),
    ])),
  }),
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    // 流式更新
    onUpdate({ type: "text", text: "Fetching..." });

    const response = await fetch(params.url, { signal });
    const text = await response.text();

    return {
      content: [{ type: "text", text }],
      details: { status: response.status },
    };
  },
});

13.11.2 覆盖内置工具

通过注册同名工具,可以覆盖内置工具的行为:

pi.registerTool({
  name: "bash", // 覆盖内置 bash 工具
  // ...自定义实现
});

13.11.3 远程执行

工具可以在远程服务器上执行命令:

async execute(toolCallId, params, signal, onUpdate, ctx) {
  // 通过 SSH 在远程服务器执行
  const result = await sshExec(params.host, params.command);
  return {
    content: [{ type: "text", text: result.stdout }],
    details: { exitCode: result.exitCode },
  };
}

13.11.4 输出截断

对于大量输出,使用 onUpdate 进行流式传输:

async execute(toolCallId, params, signal, onUpdate, ctx) {
  const stream = await fetchLargeData(params.url);

  for await (const chunk of stream) {
    if (signal.aborted) break;
    onUpdate({ type: "text", text: chunk });
  }

  return { content: [{ type: "text", text: "Done" }], details: {} };
}

13.11.5 动态工具加载

根据上下文动态注册/注销工具:

pi.on("session_start", async () => {
  const projectType = await detectProjectType();

  if (projectType === "react") {
    pi.registerTool({ name: "component_gen", /* ... */ });
  } else if (projectType === "python") {
    pi.registerTool({ name: "test_runner", /* ... */ });
  }
});

13.12 自定义 UI

13.12.1 对话框

const result = await ctx.ui.custom((tui, theme, keybindings, done) => {
  return new MyDialog({
    theme,
    keybindings,
    onSelect: (value) => done(value),
    onCancel: () => done(null),
  });
});

13.12.2 组件与 Widget

通过 ctx.ui.custom() 在编辑器上方或下方渲染组件:

pi.on("session_start", async (_event, ctx) => {
  // 显示一个状态 widget
  ctx.ui.custom((tui) => {
    const widget = new StatusBar();
    const interval = setInterval(() => {
      widget.update();
      tui.requestRender();
    }, 1000);
    return widget;
  });
});

13.12.3 自动完成

注册自动完成提供者,为用户输入提供建议:

pi.registerAutocompleteProvider(async (input, ctx) => {
  if (input.startsWith("/deploy")) {
    return ["production", "staging", "development"];
  }
  return [];
});

13.12.4 消息渲染

自定义消息和条目的渲染方式:

pi.registerMessageRenderer("custom_type", (message, theme) => {
  return [`✨ ${message.data.title} ✨`];
});

pi.registerEntryRenderer("bash_log", (entry, theme) => {
  return [`📝 ${entry.data.command}${entry.data.success ? "✓" : "✗"}`];
});

13.12.5 Markdown 变换

在 Markdown 渲染前对其进行变换:

pi.registerMarkdownTransformer((text) => {
  // 将 TODO: 替换为 emoji
  return text.replace(/TODO:/g, "📋");
});

13.12.6 主题颜色

在扩展中访问当前主题的颜色:

pi.on("session_start", async (_event, ctx) => {
  // 通过 theme 对象获取颜色
  const accentColor = ctx.theme?.colors.accent;
});

13.13 错误处理

扩展中的错误会被 Pi 捕获并显示给用户,不会导致崩溃:

pi.registerTool({
  name: "risky_tool",
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    try {
      const result = await riskyOperation();
      return { content: [{ type: "text", text: result }], details: {} };
    } catch (error) {
      // 返回错误信息给 LLM
      return {
        content: [{ type: "text", text: `Error: ${error.message}` }],
        details: { error: true },
        isError: true,
      };
    }
  },
});

13.14 模式行为

扩展在不同运行模式下行为不同:

模式 ctx.hasUI UI 方法 说明
Interactive true 全部可用 完整 TUI 支持
Print false notify 可用 无交互式 UI
RPC false 通过 RPC 协议 取决于客户端
JSON false 不可用 纯事件流
Warning

在编写需要 UI 交互的扩展时,始终检查 ctx.hasUI,确保在非交互模式下也能正常工作。

13.15 完整示例

以下是一个完整的扩展示例,结合了事件监听、自定义工具和命令:

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

export default function (pi: ExtensionAPI) {
  // 状态
  let todoList: string[] = [];

  // 会话启动时加载状态
  pi.on("session_start", async () => {
    pi.sendMessage(`Loaded with ${todoList.length} pending items`);
  });

  // 注册 TODO 工具
  pi.registerTool({
    name: "todo_add",
    label: "Add Todo",
    description: "Add an item to the todo list",
    parameters: Type.Object({
      item: Type.String({ description: "Todo item text" }),
    }),
    async execute(_id, params) {
      todoList.push(params.item);
      return {
        content: [{ type: "text", text: `Added: ${params.item}. Total: ${todoList.length}` }],
        details: {},
      };
    },
  });

  pi.registerTool({
    name: "todo_list",
    label: "List Todos",
    description: "List all todo items",
    parameters: Type.Object({}),
    async execute() {
      const text = todoList.length === 0
        ? "No items"
        : todoList.map((t, i) => `${i + 1}. ${t}`).join("\n");
      return { content: [{ type: "text", text }], details: {} };
    },
  });

  // 注册命令
  pi.registerCommand("todo", {
    description: "Show todo list",
    handler: async (_args, ctx) => {
      ctx.ui.notify(`${todoList.length} items in todo list`, "info");
    },
  });

  // 会话关闭时保存
  pi.on("session_shutdown", async () => {
    pi.appendEntry("todo_state", { items: todoList });
  });
}

更多扩展示例请参考 GitHub 官方示例库