md-tools 变成 Pi 的原生能力:用 pi.registerTool 注册一个让模型直接调用的「检查 markdown」工具,再用 pi.registerCommand 加一个 /md 斜杠命令。这是 M05 贯穿项目的地基。registerTool:给模型一把新工具
pi.registerTool 注册的工具会出现在系统提示词里,模型可以直接调用它。核心字段:
| 字段 | 作用 |
|---|---|
name |
工具名(模型调用时用) |
label |
展示名 |
description |
给模型看的说明(决定它何时调用) |
parameters |
参数 schema(用 typebox 的 Type) |
execute |
真正执行逻辑 |
官方强调:promptSnippet 让它进入「Available tools」一行式条目,promptGuidelines 往默认提示词加「工具专属建议」。
对 md-tools,一个检查工具:
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
pi.registerTool({
name: "md_tools",
label: "MD Tools",
description: "处理 markdown 文件:stats 统计字数/段落/标题,convert 转纯文本,rename 改名(先预览)",
parameters: Type.Object({
action: StringEnum(["stats", "convert", "rename"] as const),
path: Type.String({ description: "目标 markdown 文件路径" }),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
// 实际调用你的 index.js,这里示意
return {
content: [{ type: "text", text: `[md-tools] 对 ${params.path} 执行 ${params.action}` }],
details: {},
};
},
});
注意用 StringEnum(来自 @earendil-works/pi-ai)而不是 Type.Union/Type.Literal——官方明确说后者在 Google 的 API 上不工作。
registerCommand:给你自己一条斜杠命令
pi.registerCommand 注册的是人用的斜杠命令(比如 /md)。官方给了 stats 的例子:
pi.registerCommand("stats", {
description: "Show session statistics",
handler: async (args, ctx) => {
const count = ctx.sessionManager.getEntries().length;
ctx.ui.notify(`${count} entries`, "info");
},
});
对我们:
pi.registerCommand("md", {
description: "用 md-tools 处理 markdown 文件",
handler: async (args, ctx) => {
// args 是命令后的参数,例如 "stats README.md"
ctx.ui.notify(`md-tools 收到: ${args}`, "info");
},
});
同名命令冲突:多个扩展注册同名命令时,Pi 会都保留并加数字后缀(如 /review:1、/review:2),按加载顺序。
给命令加参数自动补全
pi.registerCommand 还能配 getArgumentCompletions,给 /command ... 加自动补全。官方 deploy 的例子:
pi.registerCommand("deploy", {
description: "Deploy to an environment",
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
const envs = ["dev", "staging", "prod"];
const items = envs.map((e) => ({ value: e, label: e }));
const filtered = items.filter((i) => i.value.startsWith(prefix));
return filtered.length > 0 ? filtered : null;
},
handler: async (args, ctx) => {
ctx.ui.notify(`Deploying: ${args}`, "info");
},
});
对 md-tools,你可以补全 stats / convert / rename 这几个动作,再补当前目录的 .md 文件。
错误处理:工具「抛异常」才叫失败
官方强调一个关键点:
要标记工具执行失败(
isError: true),必须从execute里throw一个错误。只return值,无论返回值里放什么属性,都不会被标记为失败。
async execute(toolCallId, params) {
if (!isValid(params.input)) {
throw new Error(`Invalid input: ${params.input}`);
}
return { content: [{ type: "text", text: "OK" }], details: {} };
}
抛出的错误会被捕获、以 isError: true 报告给模型,执行继续。这对 md-tools 处理「文件不存在」这类错误很重要。
落地练习:注册你的第一个工具和命令
- 写一个扩展文件,注册
md_tools工具 +/md命令(用上面的骨架) - 放进
~/.pi/agent/extensions/或pi -e ./文件.ts启动 - 输入
/md stats,再让模型调用md_tools工具,确认两者都工作
怎么判断做对了?——
/md有响应;模型的工具列表里能看到md_tools并能成功调用。
卡住了怎么办? 工具不出现 → 确认 registerTool 的参数 schema 正确。命令没反应 → 检查 registerCommand 名字和 handler。想验证 → 用 pi -e 指定路径先测。
常见坑:字符串枚举用了 Type.Union,Google 上翻车
写参数 schema 时最容易踩的坑:字符串枚举写成 Type.Union([Type.Literal("a"), Type.Literal("b")])。
官方明确:这种写法在 Google 的 API 上不工作。 要用 StringEnum(@earendil-works/pi-ai)代替:
import { StringEnum } from "@earendil-works/pi-ai";
action: StringEnum(["stats", "convert", "rename"] as const)
如果你接了 Google 系模型,这个坑会让你工具直接失效。养成用 StringEnum 的习惯。
小结
registerTool给模型新工具,核心是parameters+execute- 字符串枚举用
StringEnum,别用Type.Union registerCommand给人用斜杠命令,可加参数补全- 工具失败要
throw,return不算失败 - 同名命令会加数字后缀共存
下一节,深入生命周期——事件顺序、状态管理、reload 等进阶用法。