# 第三章：扩展系统 -- 从"硬编码"到"可插拔"

> 对应源文件：`packages/coding-agent/src/core/extensions/types.ts`

## 为什么需要扩展系统

假设你想给 pi 添加一个"代码审查"功能。没有扩展系统时，你需要修改 pi 的源代码：改 agent-session、改 system prompt、改 TUI 渲染...

有了扩展系统，你只需要写一个 TypeScript 文件：

```typescript
// .pi/extensions/code-review.ts
export default function(ctx) {
  // 注册工具
  ctx.registerTool({
    name: "review",
    description: "Review code changes",
    parameters: Type.Object({ path: Type.String() }),
    execute: async (id, { path }) => { ... },
  });

  // 注册斜杠命令
  ctx.registerCommand("/review", {
    description: "Start code review",
    handler: async (cmdCtx) => { ... },
  });

  // 监听事件
  ctx.on("agent_end", async (event, handlerCtx) => {
    // 每次 AI 完成后做点什么
  });
}
```

## 扩展能做什么

扩展 API（`ExtensionContext` + `ExtensionUIContext`）提供了几乎覆盖所有层面的能力：

### 1. 注册工具

```typescript
ctx.registerTool({
  name: "deploy",
  label: "deploy",
  description: "Deploy to production",
  parameters: deploySchema,
  execute: async (id, args, signal) => { ... },
  renderCall: (args, theme) => { ... },    // 自定义 TUI 渲染
  renderResult: (result, opts, theme) => { ... },
});
```

### 2. 注册命令

斜杠命令出现在输入框的 `/` 菜单中：

```typescript
ctx.registerCommand("/deploy", {
  description: "Deploy the current project",
  aliases: ["/d"],          // 快捷别名
  args: "<env>",            // 参数提示
  handler: async (cmdCtx, args) => {
    const env = args || await cmdCtx.ui.select("Select environment", ["staging", "prod"]);
    await doDeploy(env);
  },
});
```

### 3. 注册快捷键

```typescript
ctx.registerKeybinding({
  key: "ctrl+shift+d",
  description: "Quick deploy",
  handler: async (cmdCtx) => {
    // 同命令 handler
  },
});
```

### 4. 注册 CLI flags

扩展可以声明自己的 CLI 参数：

```typescript
ctx.registerFlag({
  name: "deploy-env",
  description: "Target deployment environment",
  type: "string",
});
// 用户可以 pi --deploy-env=staging
// 扩展通过 ctx.flagValues.get("deploy-env") 获取值
```

### 5. 事件监听

扩展可以订阅几乎所有事件：

```
session_start / session_shutdown       -- 会话生命周期
before_agent_start / agent_start / agent_end  -- agent 循环生命周期
turn_start / turn_end                  -- 每一轮
message_start / message_update / message_end  -- 消息流式传输
tool_execution_start / tool_execution_end     -- 工具执行
context                                -- LLM 调用前的消息列表
model_select                           -- 模型切换
input                                  -- 用户输入（可拦截/转换）
session_before_compact / session_compact      -- 压缩前后
tool_call (read/write/edit/bash/grep/find/ls) -- 内置工具调用
user_bash                              -- 用户 ! 命令
```

### 6. UI 操作

扩展可以通过 `ctx.ui` 与用户交互：

```typescript
// 选择对话框
const choice = await ctx.ui.select("选择", ["A", "B", "C"]);

// 确认对话框
const ok = await ctx.ui.confirm("确认？", "真的要执行吗？");

// 文本输入
const name = await ctx.ui.input("输入名称", "默认值");

// 通知
ctx.ui.notify("部署成功！", "info");

// 状态栏
ctx.ui.setStatus("deploy", "Deploying...");

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

// 自定义 widget
ctx.ui.setWidget("deploy-status", ["Status: deploying..."], { placement: "aboveEditor" });

// 自定义页脚
ctx.ui.setFooter((tui, theme, footerData) => new MyFooter(tui, theme));

// 自定义页眉
ctx.ui.setHeader((tui, theme) => new MyHeader(tui, theme));

// 自定义编辑器
ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
```

### 7. 会话控制

扩展命令（`ExtensionCommandContext`）还可以控制会话：

```typescript
// 创建新会话
await cmdCtx.newSession();

// Fork 会话
await cmdCtx.fork(entryId);

// 切换会话
await cmdCtx.switchSession(sessionPath);

// 导航会话树
await cmdCtx.navigateTree(targetId, { summarize: true });

// 重新加载扩展
await cmdCtx.reload();
```

---

## 扩展生命周期

```
1. 发现阶段
   ResourceLoader 扫描以下位置：
   ├── .pi/extensions/           （项目级）
   ├── ~/.pi/agent/extensions/   （全局级）
   └── --extension CLI 参数       （显式指定）

2. 加载阶段
   import() 加载 TypeScript 模块
   执行 default export 函数
   注入 ExtensionContext

3. session_start 事件
   扩展初始化完成
   可以读取历史 entries 恢复状态

4. 运行阶段
   事件驱动
   工具调用、命令处理

5. session_shutdown 事件
   清理资源
   保存状态
```

### resources_discover 事件

扩展可以在 `resources_discover` 事件中注册额外的资源路径：

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

---

## 事件钩子机制

### before 事件的取消能力

以 `session_before_compact` 为例，扩展可以阻止压缩：

```typescript
ctx.on("session_before_compact", async (event) => {
  // 自定义压缩逻辑
  const result = await myCustomCompaction(event.preparation);
  return { cancel: true, result };  // 取消默认压缩，使用自定义结果
});
```

### input 事件的拦截能力

```typescript
ctx.on("input", async (event) => {
  if (event.text.startsWith("@translate")) {
    const translated = await translate(event.text.slice(10));
    return { action: "transform", text: translated };
  }
  return { action: "continue" };  // 不干预
});
```

### context 事件的消息修改

```typescript
ctx.on("context", async (event) => {
  // 在发给 LLM 前修改消息列表
  event.messages.push({
    role: "user",
    content: `当前时间：${new Date().toISOString()}`,
    timestamp: Date.now(),
  });
});
```

---

## 扩展 vs 内置功能的边界

Pi 的设计哲学是**最小核心 + 最大扩展**。判断一个功能应该内置还是做成扩展的标准：

| 内置 | 扩展 |
|------|------|
| 所有用户都需要（read, write, edit, bash） | 特定场景需要（代码审查、部署） |
| 性能敏感（session 持久化） | 不影响核心性能 |
| 需要深度集成（TUI 渲染循环） | 可以通过 API 完成 |

实际上，很多"核心"功能也是通过扩展机制实现的：
- Sub-agent（子代理调用其他 AI）
- Plan mode（让 AI 先规划再执行）
- 权限弹窗（bash 命令的用户确认）

---

## 小结

```
扩展系统
├── 注册能力
│   ├── registerTool()      → LLM 可调用的工具
│   ├── registerCommand()   → 斜杠命令
│   ├── registerKeybinding()→ 快捷键
│   └── registerFlag()      → CLI 参数
│
├── 事件监听
│   ├── 会话事件            → start / shutdown / compact / tree
│   ├── Agent 事件          → start / end / turn / message / tool
│   └── 输入/模型事件       → input / model_select / context
│
├── UI 操作
│   ├── 对话框              → select / confirm / input / editor
│   ├── 状态栏              → setStatus / setWidget
│   └── 自定义组件          → setHeader / setFooter / setEditorComponent / custom
│
└── 会话控制
    ├── newSession / fork / switchSession
    ├── navigateTree
    └── compact / reload
```
