Skip to content

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。它提供:

  1. project_summary 工具:统计工作目录里的文件类型、关键清单与潜在风险,返回一份有上限的摘要;
  2. /guard-status 命令:显示插件版本、扫描根目录、最近一次扫描状态和当前策略;
  3. tool_call 策略钩子:当模型准备调用高风险工具时记录原因,严格模式下要求用户确认或阻止;
  4. session_start / session_shutdown 生命周期:初始化和释放本次会话资源;
  5. 可选 SKILL.md:告诉模型哪些任务应先运行 project_summary,但不把整份操作手册常驻上下文。

第一版坚持只读,避免“综合项目”同时引入文件修改的不可逆风险。等测试齐全后,再把自动修复作为第二版能力。

先画权限边界,再写代码 ​

输入或动作必须回答的问题最低安全规则
模型传入路径能否越过工作目录?符号链接怎么办?解析真实路径后确认仍位于允许根目录内
扫描文件会不会读取密钥、二进制或超大文件?默认忽略敏感模式、二进制与依赖目录;设置单文件和总量上限
工具输出会不会把上下文窗口塞满?限制条目数与字符数,并明确标记“已截断”
用户取消扫描能否及时停下?在遍历与读取边界检查 AbortSignal
工具失败模型能否理解并修正?返回结构化错误结果,不泄露无关绝对路径或密钥内容
重载与退出旧监听器、计时器是否残留?在生命周期结束时成对释放资源

这张表就是插件的威胁模型。实现过程中每增加一种外部能力,都要补一行,而不是等代码写完再“加点安全检查”。

四个开发里程碑 ​

里程碑一:最小可加载 ​

  • 创建单个 .ts 文件并导出 Extension 工厂;
  • 只注册 /guard-status,启动 Pi 后确认命令出现在补全中;
  • 运行 /reload,确认命令没有重复注册、旧闭包不再影响新会话;
  • 在文件头写明支持的 Pi 基线:本章代码以本书锁定的 v0.87.0 为学习基线,真正发布前要在目标版本复核类型。

完成这一阶段时,功能可以是空的,但加载、重载和诊断必须是确定的。

里程碑二:工具契约 ​

把 project_summary 拆成三层:

  1. Schema 层:只描述模型可传的字段,例如相对路径、最大文件数、是否包含隐藏文件;数字要用整数与上下界,枚举要用字面量联合;
  2. 语义层:处理 JSON Schema 表达不了的关系,例如“根路径不能越界”“总预算不能小于单文件预算”;
  3. 执行层:遍历、读取、聚合与截断。它只接收通过前两层的值,并在每个可能耗时的边界响应取消。

不要把校验散落在遍历循环里。单独的纯函数更容易测试,也能让错误消息保持稳定。

里程碑三:策略与状态 ​

  • 用 tool_call 事件观察高风险调用,但默认只记录,不擅自改变用户现有工作流;
  • 用闭包保存“本进程状态”,用会话条目或工具结果 details 保存需要随会话树恢复的状态(details 只能放 JSON 兼容的值,见 7.3);
  • 明确 /reload、切换分支、继续旧会话和退出进程时,各类状态应保留还是丢弃;
  • 命令输出同时说明“当前值”和“值来自哪里”,否则排查配置覆盖会很痛苦。

里程碑四:可交付验证 ​

至少覆盖下面的离线用例:

类别用例
正常小项目、空目录、混合文件类型、输出未截断/已截断
参数缺字段、类型错误、多余字段、负数、超过上限
路径../ 越界、绝对路径、符号链接越界、名称相似但不在根目录
取消扫描前取消、扫描中取消、取消后能再次运行
生命周期首次加载、/reload、会话恢复、正常退出
隐私.env、凭据文件、二进制文件不会进入工具结果

测试优先调用纯函数和工具的 execute,不需要 API Key。最后再启动一次 Pi,做命令补全、真实 Tool Call 与重载的人工验收。

交付物清单 ​

一个可供别人试用的版本至少包含:

  • Extension 源文件与必要的本地模块;
  • 自动化测试及一条可复制的测试命令;
  • README:用途、安装位置、支持的 Pi 版本、权限边界、配置、卸载方法;
  • 示例输出,同时包含成功和错误场景;
  • 变更记录,明确破坏性配置或状态格式变化;
  • 可选 Skill,并在 description 中写清触发时机,而不是泛泛写“帮助分析项目”。
🛠 实践任务完成 Project Guard 的设计与最小纵切

目标:先交付一条从注册到测试完全打通的纵切,而不是一次写完所有功能。

  1. 写一页设计说明,完成本章的权限边界表,并为每一行写一个测试名;
  2. 实现 /guard-status 与只接受 path 的最小 project_summary;
  3. 只扫描一层目录,加入路径边界、20 个条目上限与“已截断”标记;
  4. 写五个离线测试:正常目录、空目录、../、超量截断、已取消信号;
  5. 在 Pi 中加载、调用、/reload 后再调用一次;
  6. 请另一位使用者只看 README 安装并卸载,记录他需要口头追问的每个问题。

完成标准:五个测试全绿;越界时没有读取目标文件;取消能在有限时间内返回;重载后命令与工具各只有一份;新使用者不需要改源码即可安装和卸载。

进入真实开发前的版本检查 ​

本章给的是开发方法,不承诺后来版本的每个类型名和事件字段保持不变。开始开发时:

  1. 在你的目标 Pi 版本中打开 Extension 官方文档与 ExtensionAPI 类型;
  2. 全文搜索本章用到的 registerTool、registerCommand、tool_call 与生命周期事件;
  3. 对照版本基线与上游差异检查从本书锁定的 16787ad5(v0.87.0)到目标版本的修改;
  4. 让 TypeScript 编译器和目标版本测试成为最终事实来源。

本章小结 ​

  • Pi 语境里的“插件开发”主要对应 Extension;Skill 是按需知识,不执行代码。

  • 生产级差异不在注册 API 的数量,而在输入边界、取消、输出预算、生命周期、测试与交付说明。

  • Schema 校验、语义校验和执行应分层;通过结构校验不等于参数在业务上安全。

  • 先做只读的最小纵切,再扩展策略与写操作,能显著缩小不可逆错误的范围。

  • 自测问题:① 为什么 project_summary 通过 JSON Schema 校验后仍需要语义校验?② 哪些状态适合放闭包,哪些需要跟随会话树恢复?③ 一个插件能加载且命令可见,为什么仍不能算“可交付”?

✅ 自测问题参考答案先自己回答,再点开对照
  1. Schema 能检查字段、类型、整数和数值范围,却不知道解析后的真实路径是否越过项目根、两个预算字段是否互相矛盾,也不知道某个文件是否属于敏感数据;这些是依赖运行环境和业务关系的语义约束。
  2. 只在当前加载实例有效、重载后可以丢失的临时状态适合闭包;需要在恢复会话或切换会话树分支后回到当时值的状态,应写入会话条目或可从分支上的工具结果 details 重建。
  3. 加载成功只覆盖最短的快乐路径。可交付版本还必须证明失败、取消、越界、重载与卸载行为,说明权限和兼容版本,并让使用者无需阅读源码就能安装、验证和移除。

本书分析的 Pi 版本:earendil-works/pi@16787ad(2026-09-21)