为什么需要给 Pi Agent 写扩展?

如果你已经用过 Pi Coding Agent,就会发现它是一个"小而美"的终端 AI 编程助手——核心只做一件事:把 LLM 的能力串进终端工作流。但真正让它变得"专属"的,是它的扩展系统(Extensions)

Pi 的扩展是 TypeScript 模块,可以订阅生命周期事件、注册 LLM 可调用的自定义工具、添加斜杠命令、甚至自定义 TUI 组件。官方生态里已经有 pi-hosts(23 pts)、Parallel Pi agents(8 pts)等社区项目,但中文深度技术内容仍然稀缺。

如果你还没装 Pi,建议先看这篇入门:2026 年 5 大开源 AI 编程代理横评,里面有 Pi 的定位对比。本文则专注扩展开发,带你从零写出 4 个实战扩展。

Pi Agent 架构解析

核心组件

Pi 的架构可以用一句话概括:最小内核 + 可扩展边界

┌─────────────────────────────────────────────┐
│  Pi 核心内核                                 │
│  ├─ LLM Provider 层(Anthropic/OpenAI/本地) │
│  ├─ Session 管理(会话、压缩、分支)          │
│  ├─ 内置工具(bash、read、write、search…)    │
│  └─ TUI 渲染(Ink/React)                    │
└─────────────────────────────────────────────┘
        ▲            ▲            ▲
        │            │            │
   Extensions     Skills      Packages
  (TypeScript)   (Markdown)   (npm/git)
  • Extensions:TypeScript 模块,运行时加载,能拦截事件、注册工具、添加命令。
  • Skills:Markdown 文件,按需加载到上下文,定义工作流和提示词。
  • Packages:通过 npm 或 git 分发的扩展集合。

插件系统原理

Pi 的扩展加载基于 jiti,无需编译即可直接运行 TypeScript。扩展的入口是一个默认导出的工厂函数,接收 ExtensionAPI 实例:

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

export default function (pi: ExtensionAPI) {
  // 在这里订阅事件、注册工具、添加命令
}

扩展可以放在两个位置被自动发现:

位置 作用域
~/.pi/agent/extensions/*.ts 全局(所有项目)
.pi/extensions/*.ts 项目本地(需信任项目)

API 接口说明

ExtensionAPI 提供三类能力:

  1. 事件订阅pi.on("event_name", handler) — 覆盖从 session_starttool_call 的完整生命周期。
  2. 工具注册pi.registerTool({ name, description, parameters, execute }) — 让 LLM 能调用你的自定义函数。
  3. 命令注册pi.registerCommand("name", { handler }) — 添加 /name 斜杠命令。

开发环境搭建

前置条件

  • Node.js 18+(推荐 20 LTS)
  • npm 或 pnpm
  • Pi Coding Agent 已安装:npm install -g --ignore-scripts @earendil-works/pi-coding-agent

创建扩展项目

# 创建全局扩展目录
mkdir -p ~/.pi/agent/extensions/my-first-ext
cd ~/.pi/agent/extensions/my-first-ext

# 初始化 package.json
npm init -y

# 安装类型定义
npm install @earendil-works/pi-coding-agent typebox

# 创建入口文件
touch index.ts

目录结构

my-first-ext/
├── package.json
├── node_modules/
└── src/
    └── index.ts        # 入口

package.json 需要声明扩展入口:

{
  "name": "my-first-ext",
  "dependencies": {
    "@earendil-works/pi-coding-agent": "^1.0.0",
    "typebox": "^0.34.0"
  },
  "pi": {
    "extensions": ["./src/index.ts"]
  }
}

第一个扩展:Hello World

基础版:通知 + 命令

// src/index.ts
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 World 扩展已加载!", "info");
  });

  // 注册 /hello 命令
  pi.registerCommand("hello", {
    description: "向 Pi 打个招呼",
    handler: async (args, ctx) => {
      const name = args?.trim() || "World";
      ctx.ui.notify(`Hello, ${name}! 👋`, "info");
    },
  });
}

测试运行

# 方式 1:自动发现(放在 ~/.pi/agent/extensions/ 下)
pi

# 方式 2:显式指定(临时测试)
pi -e ./src/index.ts

进入 Pi 后输入 /hello Kevin,你应该看到通知:Hello, Kevin! 👋

进阶:拦截危险命令

pi.on("tool_call", async (event, ctx) => {
  if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
    const ok = await ctx.ui.confirm("⚠️ 危险操作", "确认执行 rm -rf?");
    if (!ok) return { block: true, reason: "用户取消" };
  }
});

这个扩展展示了 tool_call 事件的拦截能力——你可以在 LLM 执行工具前做权限检查。

实战项目一:自定义代码模板生成器

需求

团队有固定的代码模板(React 组件、Express 路由、Python 脚本等),每次新建文件都要复制粘贴。我们写一个扩展,让 LLM 能调用 generate_template 工具自动生成。

实现

// src/template-generator.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import * as fs from "node:fs";
import * as path from "node:path";

const TEMPLATES: Record<string, string> = {
  "react-component": `import React from 'react';

interface {{Name}}Props {
  title: string;
}

export const {{Name}}: React.FC<{{Name}}Props> = ({ title }) => {
  return <div className="{{name}}">{title}</div>;
};
`,
  "express-route": `import { Router, Request, Response } from 'express';

const router = Router();

router.get('/', (req: Request, res: Response) => {
  res.json({ message: '{{Name}} endpoint' });
});

export default router;
`,
  "python-script": `#!/usr/bin/env python3
"""{{Name}} module."""

import argparse
import sys


def main(args: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="{{Name}}")
    parser.add_argument("--name", default="World")
    parsed = parser.parse_args(args)
    print(f"Hello, {parsed.name}!")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
`,
};

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "generate_template",
    label: "模板生成器",
    description: "根据模板名和组件名生成代码文件。可选模板:react-component, express-route, python-script",
    parameters: Type.Object({
      template: Type.String({ description: "模板名称" }),
      name: Type.String({ description: "组件/模块名(PascalCase)" }),
      outputPath: Type.String({ description: "输出文件路径" }),
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
      const tpl = TEMPLATES[params.template];
      if (!tpl) {
        return {
          content: [{ type: "text", text: `❌ 未知模板: ${params.template}` }],
          details: {},
        };
      }

      const pascalName = params.name.replace(/(^|[-_])(\w)/g, (_, __, c) => c.toUpperCase());
      const kebabName = pascalName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
      const code = tpl
        .replace(/\{\{Name\}\}/g, pascalName)
        .replace(/\{\{name\}\}/g, kebabName);

      const outPath = path.resolve(params.outputPath);
      fs.mkdirSync(path.dirname(outPath), { recursive: true });
      fs.writeFileSync(outPath, code, "utf-8");

      return {
        content: [{ type: "text", text: `✅ 已生成 ${outPath}\n\n${code}` }],
        details: { path: outPath, template: params.template },
      };
    },
  });
}

使用方式

在 Pi 中直接说:

用 react-component 模板生成一个 UserProfile 组件,放到 src/components/UserProfile.tsx

Pi 会调用 generate_template 工具,自动完成变量替换和文件写入。

实战项目二:Git 工作流自动化

需求

每次提交前自动跑 lint、生成 commit message、推送后通知团队。我们用扩展拦截 bash 工具,在检测到 git commit 时注入前置检查。

实现

// src/git-workflow.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { execSync } from "node:child_process";

export default function (pi: ExtensionAPI) {
  // 拦截 git commit,前置 lint
  pi.on("tool_call", async (event, ctx) => {
    if (event.toolName !== "bash") return;
    const cmd: string = event.input.command || "";

    if (cmd.startsWith("git commit")) {
      ctx.ui.setStatus("git-workflow", "🔍 运行 pre-commit 检查...");

      try {
        execSync("npm run lint --if-present", { stdio: "pipe" });
        execSync("npm run test --if-present", { stdio: "pipe" });
        ctx.ui.setStatus("git-workflow", "✅ 检查通过,继续提交");
      } catch (err) {
        ctx.ui.notify("❌ Lint/Test 失败,已阻止提交", "error");
        return { block: true, reason: "pre-commit checks failed" };
      }
    }

    // 拦截 git push,发送通知
    if (cmd.startsWith("git push")) {
      ctx.ui.setStatus("git-workflow", "📤 推送中...");
      // 可以在这里调用 webhook 通知团队
    }
  });

  // 注册 /git-summary 命令
  pi.registerCommand("git-summary", {
    description: "显示当前分支的提交摘要",
    handler: async (_args, ctx) => {
      try {
        const log = execSync(
          "git log --oneline -10 --format='%h %s (%an, %ar)'",
          { encoding: "utf-8" }
        );
        ctx.ui.notify(`最近 10 条提交:\n${log}`, "info");
      } catch {
        ctx.ui.notify("❌ 无法读取 git 历史", "error");
      }
    },
  });
}

效果

  • 当 Pi 尝试执行 git commit 时,扩展自动跑 lint 和测试,失败则阻止提交。
  • /git-summary 命令快速查看最近提交。

实战项目三:代码质量检查集成

需求

让 LLM 在写代码时自动调用外部质量检查工具(如 eslintpylintsemgrep),并在结果中附带修复建议。

实现

// src/quality-checker.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import { execSync } from "node:child_process";

type Linter = {
  cmd: string;
  pattern: RegExp;
};

const LINTERS: Linter[] = [
  { cmd: "npx eslint --format json", pattern: /\.(ts|tsx|js|jsx)$/ },
  { cmd: "pylint --output-format=json", pattern: /\.py$/ },
  { cmd: "semgrep --json", pattern: /.*/ },
];

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "check_code_quality",
    label: "代码质量检查",
    description: "对指定文件运行 lint/静态分析,返回问题列表和修复建议",
    parameters: Type.Object({
      filePath: Type.String({ description: "要检查的文件路径" }),
      linter: Type.Optional(
        Type.String({ description: "指定 linter(eslint/pylint/semgrep),留空自动检测" })
      ),
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
      const file = params.filePath;
      let linter = LINTERS.find((l) => l.pattern.test(file));

      if (params.linter) {
        linter = LINTERS.find((l) => l.cmd.startsWith(params.linter!));
      }

      if (!linter) {
        return {
          content: [{ type: "text", text: "⚠️ 未找到匹配的 linter" }],
          details: {},
        };
      }

      ctx.ui.setStatus("quality", `🔍 运行 ${linter.cmd.split(" ")[0]}...`);

      try {
        const output = execSync(`${linter.cmd} ${file}`, {
          encoding: "utf-8",
          stdio: ["pipe", "pipe", "pipe"],
        });
        return {
          content: [{ type: "text", text: `✅ 无问题\n${output}` }],
          details: { file, linter: linter.cmd },
        };
      } catch (err: any) {
        // linter 通常以非零退出码返回问题
        const output = err.stdout || err.stderr || "";
        return {
          content: [{ type: "text", text: `⚠️ 发现问题:\n${output.slice(0, 3000)}` }],
          details: { file, linter: linter.cmd, hasIssues: true },
        };
      }
    },
  });
}

使用方式

检查一下 src/auth/login.ts 的代码质量

Pi 会调用 check_code_quality,返回 lint 结果,然后自动给出修复建议。

实战项目四:自定义搜索工具集成

需求

让 Pi 能搜索团队内部知识库(比如 Notion、Confluence、或本地 Markdown 文件)。这里以本地 Markdown 知识库为例。

实现

// src/knowledge-search.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import * as fs from "node:fs";
import * as path from "node:path";

const KB_ROOT = process.env.KB_ROOT || "~/knowledge-base";

interface SearchResult {
  file: string;
  line: number;
  context: string;
}

function searchMarkdown(query: string, root: string): SearchResult[] {
  const results: SearchResult[] = [];
  const terms = query.toLowerCase().split(/\s+/);

  function walk(dir: string) {
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
      const full = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        walk(full);
      } else if (entry.name.endsWith(".md")) {
        const content = fs.readFileSync(full, "utf-8");
        const lines = content.split("\n");
        for (let i = 0; i < lines.length; i++) {
          const lower = lines[i].toLowerCase();
          if (terms.every((t) => lower.includes(t))) {
            results.push({
              file: path.relative(root, full),
              line: i + 1,
              context: lines.slice(Math.max(0, i - 1), i + 2).join("\n"),
            });
          }
        }
      }
    }
  }

  walk(path.resolve(root));
  return results.slice(0, 10);
}

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "search_knowledge_base",
    label: "知识库搜索",
    description: "在本地 Markdown 知识库中搜索相关内容",
    parameters: Type.Object({
      query: Type.String({ description: "搜索关键词" }),
      kbRoot: Type.Optional(Type.String({ description: "知识库根目录,默认 ~/knowledge-base" })),
    }),
    async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
      const root = params.kbRoot || KB_ROOT;
      const resolved = path.resolve(root.replace("~/", process.env.HOME + "/"));

      if (!fs.existsSync(resolved)) {
        return {
          content: [{ type: "text", text: `❌ 知识库目录不存在: ${resolved}` }],
          details: {},
        };
      }

      const results = searchMarkdown(params.query, resolved);
      if (results.length === 0) {
        return {
          content: [{ type: "text", text: "🔍 未找到相关内容" }],
          details: { query: params.query },
        };
      }

      const formatted = results
        .map((r) => `📄 ${r.file}:${r.line}\n${r.context}`)
        .join("\n\n");

      return {
        content: [{ type: "text", text: `找到 ${results.length} 处匹配:\n\n${formatted}` }],
        details: { query: params.query, count: results.length },
      };
    },
  });
}

使用方式

设置环境变量 KB_ROOT=~/my-docs,然后在 Pi 中:

搜一下知识库里关于 API 认证的内容

Pi 会调用 search_knowledge_base,返回匹配的文件片段。

高级主题

多 Agent 协作

Pi 支持通过扩展实现多 Agent 模式。核心思路是:主 Agent 接收任务,拆分后委托给子 Agent(通过 bash 工具启动新的 pi 进程,使用 --resume 或 session 文件共享上下文)。

pi.registerTool({
  name: "delegate_to_specialist",
  label: "委托专家",
  description: "将子任务委托给专门的 Agent 处理",
  parameters: Type.Object({
    task: Type.String({ description: "子任务描述" }),
    specialist: Type.String({ description: "专家类型:frontend/backend/test" }),
  }),
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
    const sessionFile = `/tmp/pi-${params.specialist}-${Date.now()}.json`;
    const prompt = encodeURIComponent(params.task);

    ctx.ui.setStatus("delegate", `🤖 委托 ${params.specialist} 处理...`);

    try {
      const output = execSync(
        `pi --session ${sessionFile} --print "${params.task}"`,
        { encoding: "utf-8", timeout: 120_000 }
      );
      return {
        content: [{ type: "text", text: output }],
        details: { specialist: params.specialist, session: sessionFile },
      };
    } catch (err: any) {
      return {
        content: [{ type: "text", text: `❌ 委托失败: ${err.message}` }],
        details: {},
      };
    }
  },
});

性能优化

  • 延迟加载:不要在工厂函数里启动后台进程,推迟到 session_start 或首次工具调用。
  • 缓存结果:对频繁调用的工具(如 lint),用 Map 缓存文件哈希和结果,避免重复执行。
  • 流式更新:用 onUpdate 回调实时反馈进度,避免 UI 卡死。
// 缓存示例
const cache = new Map<string, string>();

async execute(_toolCallId, params) {
  const hash = crypto.createHash("md5").update(fs.readFileSync(params.filePath)).digest("hex");
  const key = `${params.filePath}:${hash}`;
  if (cache.has(key)) return { content: [{ type: "text", text: cache.get(key)! }], details: { cached: true } };
  // ... 执行检查 ...
  cache.set(key, result);
  return { content: [{ type: "text", text: result }], details: {} };
}

发布和分享扩展

Pi 扩展可以通过 npm 或 git 仓库分发为 pi packages

// package.json
{
  "name": "pi-ext-my-tools",
  "version": "1.0.0",
  "pi": {
    "extensions": ["./src/index.ts"]
  }
}

用户安装:

# 从 npm
pi install npm:pi-ext-my-tools@1.0.0

# 从 git
pi install git:github.com/user/repo@v1

也可以在 pi.dev/packages 发布到官方包目录。

社区优秀扩展推荐

扩展 功能 链接
pi-hosts 管理 /etc/hosts,快速切换环境 GitHub
Parallel Pi agents 多 Agent 并行执行任务 GitHub
pi-extensions (narumiruna) 自动化、规划、浏览器控制、Git 工作流集合 GitHub
pi-extensions-skill (Dwsy) 渐进式扩展开发学习指南 GitHub
pi-extension-builder (LobeHub) 扩展脚手架生成器 LobeHub

常见问题 FAQ

Pi Agent 扩展和 Skills 有什么区别?

Extensions 是 TypeScript 模块,运行时加载,能拦截事件、注册工具、添加命令,拥有完整的系统权限。Skills 是 Markdown 文件,按需加载到 LLM 上下文,定义工作流和提示词,不能直接执行代码。简单说:Extension 是"代码级扩展",Skill 是"提示词级扩展"。

扩展开发需要会 TypeScript 吗?

需要基础 TypeScript 能力。Pi 用 jiti 直接加载 .ts 文件,无需编译步骤。如果你只会 JavaScript,也能写——把文件后缀改成 .js 即可,类型定义是可选的。

如何调试扩展?

pi -e ./my-ext.ts 启动,在代码里加 console.log,输出会显示在 Pi 的 TUI 底部。也可以用 ctx.ui.notify() 弹出通知。复杂逻辑建议写单元测试,用 Node.js 直接跑。

扩展会影响 Pi 的启动速度吗?

同步加载的扩展会阻塞启动。建议把耗时操作(网络请求、文件扫描)推迟到 session_start 事件或首次工具调用时执行。工厂函数里只做 pi.on()pi.registerTool() 这类轻量注册。

可以在扩展里调用外部 API 吗?

可以。扩展运行在 Node.js 环境,能用 fetchhttp、任何 npm 包。记得在 package.jsondependencies 里声明依赖。

总结

Pi Agent 的扩展系统给了开发者"改造 AI 助手"的完整能力。从拦截危险命令到注册自定义工具,从 Git 自动化到知识库搜索,TypeScript + 事件驱动 的组合让扩展既灵活又可控。

本文的 4 个实战项目覆盖了最常见的场景:模板生成、Git 工作流、代码质量、知识搜索。你可以直接拿去用,也可以作为模板改造出更多扩展。

下一步建议:

  1. 读官方 Extensions 文档
  2. examples/extensions/ 里的示例
  3. pi.dev/packages 找灵感,或发布你的第一个扩展