為什麼需要為 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 提供三類能力:
- 事件訂閱:
pi.on("event_name", handler)— 涵蓋從session_start到tool_call的完整生命週期。 - 工具註冊:
pi.registerTool({ name, description, parameters, execute })— 讓 LLM 能呼叫你的自訂函式。 - 命令註冊:
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 在寫程式碼時自動呼叫外部品質檢查工具(如 eslint、pylint、semgrep),並在結果中附带修復建議。
實作
// 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 環境,能用 fetch、http、任何 npm 套件。記得在 package.json 的 dependencies 裡宣告相依性。
總結
Pi Agent 的擴充系統給了開發者「改造 AI 助手」的完整能力。從攔截危險命令到註冊自訂工具,從 Git 自動化到知識庫搜尋,TypeScript + 事件驅動的組合讓擴充既靈活又可控。
本文的 4 個實戰專案覆蓋了最常見的場景:模板產生、Git 工作流、程式碼品質、知識搜尋。你可以直接拿去用,也可以作為模板改造出更多擴充。
下一步建議:
- 讀官方 Extensions 文件
- 看 examples/extensions/ 裡的範例
- 在 pi.dev/packages 找靈感,或發佈你的第一個擴充