7.5 综合项目:从示例 Extension 到可交付插件
本页分析版本earendil-works/pi@16787ad2026-09-21本章解决什么问题:前四章分别讲了 Extension、Skill、工具、命令和 Provider,但“看懂一个例子”还不等于“能交付一个别人敢装的插件”。本章用一个综合项目把注册、生命周期、安全边界、错误处理、验证与文档串成完整开发流程。
前置知识:7.1 Extension 系统、7.2 Skill 系统、7.3 自定义工具与斜杠命令、5.6 取消与错误处理。
学习目标:完成本章后你能
- 把需求拆成工具、命令、生命周期事件与持久化边界;
- 给模型输入建立结构校验、语义校验、路径边界与输出上限;
- 用离线测试证明成功、失败、取消和重载路径;
- 写出安装、权限、卸载与兼容版本说明,让另一个人可以安全试用。
先把术语说准
日常会把这类能力叫“Pi 插件”,但 Pi 源码与官方文档中的正式机制名是 Extension。Extension 是被 Pi 加载和执行的 TypeScript 模块;Skill 是给模型按需读取的 Markdown 知识。这个综合项目主要产出 Extension,可选再配一个 Skill 教模型何时使用它。
项目题目:Project Guard
你要实现一个只读的项目体检插件,名字暂定 project-guard。它提供:
project_summary工具:统计工作目录里的文件类型、关键清单与潜在风险,返回一份有上限的摘要;/guard-status命令:显示插件版本、扫描根目录、最近一次扫描状态和当前策略;tool_call策略钩子:当模型准备调用高风险工具时记录原因,严格模式下要求用户确认或阻止;session_start/session_shutdown生命周期:初始化和释放本次会话资源;- 可选
SKILL.md:告诉模型哪些任务应先运行project_summary,但不把整份操作手册常驻上下文。
第一版坚持只读,避免“综合项目”同时引入文件修改的不可逆风险。等测试齐全后,再把自动修复作为第二版能力。
先画权限边界,再写代码
| 输入或动作 | 必须回答的问题 | 最低安全规则 |
|---|---|---|
| 模型传入路径 | 能否越过工作目录?符号链接怎么办? | 解析真实路径后确认仍位于允许根目录内 |
| 扫描文件 | 会不会读取密钥、二进制或超大文件? | 默认忽略敏感模式、二进制与依赖目录;设置单文件和总量上限 |
| 工具输出 | 会不会把上下文窗口塞满? | 限制条目数与字符数,并明确标记“已截断” |
| 用户取消 | 扫描能否及时停下? | 在遍历与读取边界检查 AbortSignal |
| 工具失败 | 模型能否理解并修正? | 返回结构化错误结果,不泄露无关绝对路径或密钥内容 |
| 重载与退出 | 旧监听器、计时器是否残留? | 在生命周期结束时成对释放资源 |
这张表就是插件的威胁模型。实现过程中每增加一种外部能力,都要补一行,而不是等代码写完再“加点安全检查”。
四个开发里程碑
里程碑一:最小可加载
- 创建单个
.ts文件并导出 Extension 工厂; - 只注册
/guard-status,启动 Pi 后确认命令出现在补全中; - 运行
/reload,确认命令没有重复注册、旧闭包不再影响新会话; - 在文件头写明支持的 Pi 基线:本章代码以本书锁定的 v0.87.0 为学习基线,真正发布前要在目标版本复核类型。
完成这一阶段时,功能可以是空的,但加载、重载和诊断必须是确定的。
里程碑二:工具契约
把 project_summary 拆成三层:
- Schema 层:只描述模型可传的字段,例如相对路径、最大文件数、是否包含隐藏文件;数字要用整数与上下界,枚举要用字面量联合;
- 语义层:处理 JSON Schema 表达不了的关系,例如“根路径不能越界”“总预算不能小于单文件预算”;
- 执行层:遍历、读取、聚合与截断。它只接收通过前两层的值,并在每个可能耗时的边界响应取消。
不要把校验散落在遍历循环里。单独的纯函数更容易测试,也能让错误消息保持稳定。
里程碑三:策略与状态
- 用
tool_call事件观察高风险调用,但默认只记录,不擅自改变用户现有工作流; - 用闭包保存“本进程状态”,用会话条目或工具结果
details保存需要随会话树恢复的状态(details只能放 JSON 兼容的值,见 7.3); - 明确
/reload、切换分支、继续旧会话和退出进程时,各类状态应保留还是丢弃; - 命令输出同时说明“当前值”和“值来自哪里”,否则排查配置覆盖会很痛苦。
里程碑四:可交付验证
至少覆盖下面的离线用例:
| 类别 | 用例 |
|---|---|
| 正常 | 小项目、空目录、混合文件类型、输出未截断/已截断 |
| 参数 | 缺字段、类型错误、多余字段、负数、超过上限 |
| 路径 | ../ 越界、绝对路径、符号链接越界、名称相似但不在根目录 |
| 取消 | 扫描前取消、扫描中取消、取消后能再次运行 |
| 生命周期 | 首次加载、/reload、会话恢复、正常退出 |
| 隐私 | .env、凭据文件、二进制文件不会进入工具结果 |
测试优先调用纯函数和工具的 execute,不需要 API Key。最后再启动一次 Pi,做命令补全、真实 Tool Call 与重载的人工验收。
交付物清单
一个可供别人试用的版本至少包含:
- Extension 源文件与必要的本地模块;
- 自动化测试及一条可复制的测试命令;
- README:用途、安装位置、支持的 Pi 版本、权限边界、配置、卸载方法;
- 示例输出,同时包含成功和错误场景;
- 变更记录,明确破坏性配置或状态格式变化;
- 可选 Skill,并在 description 中写清触发时机,而不是泛泛写“帮助分析项目”。
目标:先交付一条从注册到测试完全打通的纵切,而不是一次写完所有功能。
- 写一页设计说明,完成本章的权限边界表,并为每一行写一个测试名;
- 实现
/guard-status与只接受path的最小project_summary; - 只扫描一层目录,加入路径边界、20 个条目上限与“已截断”标记;
- 写五个离线测试:正常目录、空目录、
../、超量截断、已取消信号; - 在 Pi 中加载、调用、
/reload后再调用一次; - 请另一位使用者只看 README 安装并卸载,记录他需要口头追问的每个问题。
完成标准:五个测试全绿;越界时没有读取目标文件;取消能在有限时间内返回;重载后命令与工具各只有一份;新使用者不需要改源码即可安装和卸载。
进入真实开发前的版本检查
本章给的是开发方法,不承诺后来版本的每个类型名和事件字段保持不变。开始开发时:
- 在你的目标 Pi 版本中打开 Extension 官方文档与
ExtensionAPI类型; - 全文搜索本章用到的
registerTool、registerCommand、tool_call与生命周期事件; - 对照版本基线与上游差异检查从本书锁定的
16787ad5(v0.87.0)到目标版本的修改; - 让 TypeScript 编译器和目标版本测试成为最终事实来源。
本章小结
Pi 语境里的“插件开发”主要对应 Extension;Skill 是按需知识,不执行代码。
生产级差异不在注册 API 的数量,而在输入边界、取消、输出预算、生命周期、测试与交付说明。
Schema 校验、语义校验和执行应分层;通过结构校验不等于参数在业务上安全。
先做只读的最小纵切,再扩展策略与写操作,能显著缩小不可逆错误的范围。
自测问题:① 为什么
project_summary通过 JSON Schema 校验后仍需要语义校验?② 哪些状态适合放闭包,哪些需要跟随会话树恢复?③ 一个插件能加载且命令可见,为什么仍不能算“可交付”?
✅ 自测问题参考答案先自己回答,再点开对照
- Schema 能检查字段、类型、整数和数值范围,却不知道解析后的真实路径是否越过项目根、两个预算字段是否互相矛盾,也不知道某个文件是否属于敏感数据;这些是依赖运行环境和业务关系的语义约束。
- 只在当前加载实例有效、重载后可以丢失的临时状态适合闭包;需要在恢复会话或切换会话树分支后回到当时值的状态,应写入会话条目或可从分支上的工具结果
details重建。 - 加载成功只覆盖最短的快乐路径。可交付版本还必须证明失败、取消、越界、重载与卸载行为,说明权限和兼容版本,并让使用者无需阅读源码就能安装、验证和移除。