最近我一直在用 Pi,也装了一些别人写的 Extension。

用着用着,就会碰到一些想自己改的地方。别人觉得顺手的功能,未必完全符合我的习惯;装得多了,还得留意有没有重复功能,会不会互相影响。于是,我开始有了自己写 Extension 的念头。

前面发布相关内容的时候,也有人问过我,新手应该怎么开始自己的第一个 Extension。这篇就从这里讲起。

我想拿任务结束提醒来做个例子。需求容易说清楚,做出来以后也用得上。第一次动手,先给自己的 Pi 补上这样一个小功能,就够了。

一、先弄明白 Extension 能做什么

Extension 就是用来扩展 Pi 行为的一段程序。

Pi 官方扩展文档

列出了可以接入的位置。

你可以让它在某件事发生后做一个动作,也可以给 Pi 增加自己的命令,或者调整下面的状态栏。这篇要做的提醒,就是让它等 Pi 这一轮处理结束,再发出通知。

刚接触 Pi 的时候,可能还会遇到 Skill、Package 这些名字。放在一起看,会更容易分清。

名称 主要用途 放在这篇里怎么理解
Skill 提供完成某类工作的说明和相关材料 告诉 Pi 按什么方法做事
Extension 用代码扩展 Pi 的运行行为 让 Pi 结束处理时触发提醒
Package 组织和分发扩展、Skill、主题等资源 方便安装和分享做好的东西

写第一个 Extension,可以从一个 .ts 文件开始。暂时用不着先学会打包发布,也不用先把所有接口研究一遍。官方就有

通知扩展示例

,可以让 Pi 参考它,再按自己的需要改。

代码可以请 Pi 帮忙写。你需要把需求说清楚,并且看一眼它生成了什么、把文件放在哪里。Extension 会实际运行代码,安装别人的扩展时也一样,要知道它的来源和用途。

二、先说清楚我想要怎样的提醒

我的需求很具体。给 Pi 发出一个请求后,我想切去浏览器做别的事。等它这一轮处理结束,桌面上出现一条提醒,我再回来看看结果。

这条提醒要能在切换软件后看到。只在 Pi 对话界面里多显示一行字,对这个场景帮助不大。

第一版先用固定文案。

Pi 这一轮处理结束了,回来看看结果

这里没有直接写“任务成功”。Pi 停下来的时候,有可能已经给出了结果,也有可能遇到了错误,或者被手动中断。提醒负责叫我回来,结果还得自己看。

通知历史、手机推送这些先不加。等这个小功能能正常用,再看有没有必要继续做。需求越具体,后面就越容易判断生成的东西是不是自己要的。

三、用一个提醒扩展,走完第一次制作

下面按 macOS 加 iTerm2 来讲。准备这篇时,本机是 Pi 0.84.3、iTerm2 3.6.11。你需要已经能启动 Pi,并且能正常提交一个任务。使用其他终端的朋友,可以参考这个过程,让 Pi 根据自己的环境调整通知方式。

\1. 建一个练习目录,把需求交给 Pi

先在普通终端里执行下面几行。此时还没有进入 Pi 的对话界面。

1
2
3
mkdir -p ~/pi-notify-practice
cd ~/pi-notify-practice
pi

这里单独建个目录,方便看清新文件,也方便试完以后停用。进入 Pi 后,把下面这段需求发给它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
帮我写自己的第一个 Pi Extension,只做任务结束提醒。

我使用 macOS 和 iTerm2。先检查当前 Pi 版本,阅读当前安装版本的
docs/extensions.md 和 examples/extensions/notify.ts。找不到本地文档时,
再查 Pi 官方文档,不要凭印象编接口。

在当前目录创建 .pi/extensions/task-notify.ts,保持为一个文件。
当 Pi 这一轮处理彻底结束、不再自动继续时,发出一次桌面通知。
当前版本支持时使用 agent_settled,通知通道使用 iTerm2 的 OSC 9。
文案固定为“Pi 这一轮处理结束了,回来看看结果”。

额外提供 /notify-test 命令,方便我在 Pi 空闲时手动测试通知。
通知只在 TUI 模式且标准输出是真终端时发送。
不要注册模型 Tool,不安装额外依赖,不连接外部消息服务,
不要修改全局配置或系统通知权限。

完成后告诉我文件位置、怎样加载、怎样测试,以及怎样停用。
若当前版本不支持所需接口,先说明具体差异。

这段需求把触发时间、通知内容和文件位置都交代了,也说明了第一版做到哪里为止。Pi 生成代码后,先确认文件确实写到了 .pi/extensions/task-notify.ts,不要停在聊天里展示代码就算完成。

这也是我现在写 Extension 比较明显的一个感受。第一次只解决一个小问题。 你把它用起来以后,会更容易知道下一步该加什么。

下面是这次真实生成会话的记录。第一张保留了需求、运行环境和保存位置,第二张把生成代码、文件写入结果和测试方法放大,方便核对关键内容。

\2. 让 Pi 加载这个文件

文件生成了,还要让 Pi 重新读取它。在 Pi 的输入框里输入下面这个命令。

1
/reload

如果出现项目扩展的信任提示,确认当前目录和代码是自己刚才准备的内容,再决定加载。项目里的扩展需要在项目受信任后才会启用。第一次拿不准有没有加载,可以退出 Pi,在这个练习目录重新启动,留意启动时的提示和报错。

Pi 空闲后,输入下面这个命令测试。

1
/notify-test

它直接测试通知,不需要再让模型回答一个问题。如果提示找不到命令,先检查文件位置和加载错误;如果命令可用却看不到桌面通知,再查 iTerm 和系统的通知设置。这样能少绕一些弯路。

这里也顺便说一下 Global 和 Project。Pi 常用的自动加载位置有两个。

文件放置位置 使用范围 当前项目的 .pi/extensions/ 当前项目 用户目录的 ~/.pi/agent/extensions/ 多个项目共用

任务提醒属于比较通用的功能,试顺手以后可以移到全局目录。项目自己的部署、数据库操作或特殊规则,我更愿意留在项目里。每次打开 Pi,都能知道当前启用了哪些功能。

如果把这个文件移到全局,项目里的同一份就一起移走。两个位置都放着,可能会把同一个通知发两遍。

\3. 看懂这份小扩展,再跑一次任务

如果想先用一份现成代码走完整个过程,也可以把下面的内容保存为 .pi/extensions/task-notify.ts,然后回到上一步重新加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";

const NOTIFICATION_BODY = "Pi 这一轮处理结束了,回来看看结果";

// 参考官方 examples/extensions/notify.ts;这里按用户环境仅使用 iTerm2 的 OSC 9,
// 不声明该通知方式对所有终端通用。
function sendTaskNotification(ctx: ExtensionContext): void {
if (ctx.mode !== "tui") return;
if (!process.stdout.isTTY) return;
if (!ctx.isIdle()) return;

process.stdout.write(`\x1b]9;${NOTIFICATION_BODY}\x07`);
}

export default function (pi: ExtensionAPI) {
pi.on("agent_settled", async (_event, ctx) => {
sendTaskNotification(ctx);
});

pi.registerCommand("notify-test", {
description: "测试 Pi 任务结束通知通道",
handler: async (_args, ctx) => {
sendTaskNotification(ctx);
},
});
}

不用一行一行背代码。先找到中间的 agent_settled,它负责等 Pi 这次处理停稳。按照官方的事件说明,agent_end 后面还可能有自动重试或排队消息,做这种结束提醒时要考虑这些后续动作。

再看上面的 process.stdout.write,它把通知指令交给终端。这里用的是

iTerm2 支持的 OSC 9

。能不能显示横幅,还要看终端和系统设置,所以换终端时需要重新验证。

前面几个判断则把通知限制在终端交互界面,而且只在 Pi 空闲时发送。后面的 /notify-test 让你能单独试这条通知通道。第一次了解这些,就已经足够动手改文案了。

先发一个短请求试试。

1
给我三条周末出行准备建议,每条十个字以内。

发送后切到浏览器,观察有没有收到提醒,再回到 Pi 看结果。任务太短、来不及切换时,可以让它写一份稍长的出行清单。重点是走完“提交请求、离开 Pi、收到提醒、回来查看”的过程。

同一轮处理正常结束时,预期收到一条提醒。如果出现两条,先检查有没有同时加载别的通知扩展,或全局与项目各放了一份。

下面这张保留了扩展文件检查和 Pi 给出的行为说明。它能证明文件已经生成,也能核对手动测试命令。桌面横幅仍要以 iTerm 的实际画面为准。

准备文章时,我在独立的 Pi 交互会话里执行了 /notify-test,原始文案确实出现在 iTerm 通知里。修改文案并重新启动 Pi 后,再执行一次测试命令,通知也换成了新文字。这里验证的是当前这套 macOS 与 iTerm2 环境,换到其他终端仍要重新测试。

这个功能没有给模型增加新的 Tool。Extension 可以提供工具,也可以只监听事件、增加用户命令,这次用后两项就够了。

我现在会比较留意这件事。扩展写着写着,很容易把想到的功能全加进去。实际要给模型用哪些工具、要加载什么内容,最好跟着需求来,缺了再补。

\4. 改一句话,体验自己的第一次修改

找到文件最上面的 NOTIFICATION_BODY,把引号里的文字改成自己习惯的说法。比如下面这样。

1
const NOTIFICATION_BODY = "Pi 这边处理结束啦,回来看看";

保存文件,在 Pi 里执行 /reload,等它加载完成,再输入 /notify-test。这次要看的是通知有没有换成新文案,以及有没有重复出现。改完文件就直接测,很容易还在测旧代码。

不想自己改,也可以告诉 Pi,只修改通知文案,其他行为保持原样。改动越小,越容易看清它到底改了哪里。

/reload 确实很适合这样来回试。以后想加提醒开关,再多想一步,关闭状态要不要在重新加载后保留。只存在内存里的变量,重新加载后未必还在;需要保留的设置,要另外设计保存和恢复方式。

这次的版本没有开关,也没有通知历史,先不用处理这些。后面真的嫌提醒太频繁,可以加一个耗时门槛,只提醒运行比较久的任务。每次补一个自己已经遇到的问题就好。

试完想停用,把 task-notify.ts 移出 .pi/extensions/,例如放到练习目录下的 disabled/,然后再执行 /reload。留一份文件,后面想继续改也方便。

四、第一次动手,容易卡在哪里

如果前面的效果没有出现,可以按下面这些情况检查。

遇到的情况 先检查什么
文件生成了,命令却找不到 是否保存为 .ts 文件,位置是否正确,项目是否受信任,加载时有没有代码错误
Pi 里面有提示,切出去却看不到 生成的代码是否只用了 ctx.ui.notify,它显示的是 Pi 界面内提示
/notify-test 可用,却没有桌面通知 当前终端是否支持对应协议,再检查 iTerm 与 macOS 通知设置、专注模式;先在普通本地终端测试
文案改了却没生效 是否保存并重新加载,正在修改的文件是否就是实际加载的那份
同一轮结束提醒了两次 检查全局和项目的重复副本,以及其他同类扩展,逐个停用定位
旧版本运行时报接口错误 让 Pi 对照当前安装版本的文档检查,本文代码按 Pi 0.84.3 核验

手动中断后收到提醒,也不一定是扩展出错。这里监听的是停止自动处理的时机,文案特意让你回来查看结果。如果以后只想在成功时提醒,就得先定义自己怎样判断成功,再继续修改。

其他终端可以参考官方通知示例,选择它支持的方式。通过 SSH 运行时,显示通知的通常是你本地使用的终端;中间还有 tmux 等工具时,也要检查通知指令能不能传过去。这些可以放到后面有需要时再研究。

最后

我刚开始用 Pi 的时候,也会到处找 Extension。后来开始删重复的,再慢慢补一些自己缺的功能。

现在我会先看一个扩展解决什么问题,自己有没有这个需求。现成的合适就用,有具体不满意的地方,也可以借助 Pi 开放的扩展能力自己改。工作台要慢慢搭,先从一个任务提醒开始就挺好。