博客

Pi 扩展推荐:20 个扩展的无人值守实测

系列:Orbi 怎么搭在 Pi coding agent 上

20 个 Pi 扩展的无人值守实测:MCP、联网搜索、子代理各用哪个,哪些会改配置。

本页目录 · 8 节
  1. 无人值守时,我会留下哪些 Pi 扩展?
    1. Pi MCP 扩展
    2. Pi 联网搜索扩展
    3. Pi 子代理扩展
    4. 任务清单与命令拦截
  2. Pi 扩展怎么安装和试用?
    1. Pi 扩展和 skill 有什么区别?
  3. 20 个 Pi 扩展按用途对比
  4. 这次 Pi 扩展怎么测
  5. 无人值守跑 Pi,哪些地方会出问题
    1. MCP 适配器改的设置会影响下次会话
    2. 包名拼错,Pi 也可能以 0 退出
    3. 提问和计划评审工具需要 UI
    4. 默认权限配置会拦住普通操作
    5. npm 跳过了安装脚本
    6. 状态文件不全在 agent 目录里
    7. 钥匙串测试没有复现 9 月的卡死
  6. 在 Orbi 里固定选中的扩展版本
  7. 留下一个扩展之前,自己跑一次
  8. 相关

Pi 是一个开源的编程 agent,Pi 扩展给它加上 MCP、联网搜索、子代理这些工具。这篇按用途推荐该装哪些扩展,依据是 2026 年 11 月 5 日对 20 个热门扩展的实测。我是按 CI 任务、后台服务那样无人值守地跑 Pi 来测的,因为扩展在这种场景里最容易悄悄出问题:加载得好好的,模型却拿不到你以为有的工具,或者它改了下一次运行的配置。按用途先看这几个:

  • 网页搜索:pi-web-access 或 pi-web-search,这两个不用搜索 key 就能用。
  • 子代理:pi-subagents、@tintinweb/pi-subagents 或 pi-ptc-subagents,三个都返回了结果。pi-subagent-manager 也能用,只是用 --no-session 关掉会话时会失败。
  • MCP:pi-mcp-extension 或 @fitchmultz/pi-mcp-adapter。两个都能正常加载,也不会改你的设置;这次没有连接 MCP 服务器,这部分请用你自己的服务器验证。装得最多的 pi-mcp-adapter 也能用,但会改写设置,每次运行要给它一个新的 agent 目录。
  • 任务清单:rpiv-todo。
  • 命令拦截:cc-safety-net;pi-permission-system 要先写明确的权限配置。

下文说明这些选择的依据,你也可以用后面的脚本自行测试扩展。

无人值守时,我会留下哪些 Pi 扩展?

Pi MCP 扩展

需要 MCP 时,pi-mcp-extension 和 @fitchmultz/pi-mcp-adapter 会是我的候选;也可以用 pi-mcp-adapter,每次运行前重建 agent 目录。这次三个扩展都没连接服务器,在留下的东西上,测到的区别只有设置文件副作用:pi-mcp-adapter 改了设置,另外两个没有。

Pi 联网搜索扩展

网页搜索方面,pi-web-access 没有搜索 key 也能用,报告的 provider 是 "Provider: exa";它的 README 说明,没有 key 时会通过 Exa 的 MCP 端点搜索,因此查询会发给 Exa。pi-web-search 也能用,它的 README 说搜索通过当前选定的模型完成。@zeldrisho/pi-web-search 和 pi-wsearch 能正常加载,但要先配好自己的搜索 key(前者用 Brave),否则只返回错误。

Pi 子代理扩展

三个子 agent 包返回了 PONG,其中 Pi-subagents 那次,模型先猜了一个 agent 名称,重试后才成功。pi-subagent-manager 在 --no-session 下启动子 agent 失败,给 Pi 一个会话目录后就能用;像 Orbi 这样的运行器本来就给每个会话一个目录。

任务清单与命令拦截

任务清单我会保留 rpiv-todo,用 cc-safety-net 拦截命令。permission-system 要有明确配置才会用。

我没测 OAuth MCP 服务器、付费搜索 key 或长时间运行。测试环境是 Linux,没有测 Windows 和 macOS。

Pi 扩展怎么安装和试用?

  • 试用一次:pi -e npm:<package>@<version>,只在这次运行加载扩展;扩展写入的配置仍可能保留。
  • 长期安装:pi install npm:<package>@<version>,用 pi list 查看、pi remove 删除;Pi 1.5.0 的 pi --help 有这些子命令。
  • 无人值守时建议钉版本,用后文脚本先测。

Pi 扩展和 skill 有什么区别?

Pi agent 扩展是 Pi 启动时加载的代码,可以来自本地文件、npm 包或 git 仓库。多数扩展会注册模型可以调用的工具,本文说的 Pi 插件也指这类扩展。skill 是写在 SKILL.md 里的指令。生产环境那篇的第一节介绍了两者的区别,以及 Pi 怎样加载扩展。

20 个 Pi 扩展按用途对比

表格按用途分组,推荐依据是这次调用结果。下载量是 2026 年 11 月 5 日查询的 npm 上月包下载总量,工具一列只列出对照组之外新增的工具。我只比较了临时 HOME 和测试仓库里的文件变化,不包含 /tmp 或系统目录。「未观察到」不计 Pi 通常会写的 auth.json、models-store.json、npm 缓存和包的安装文件。每行结论只适用于「调用测试」列的操作,钥匙串访问和计时结果见后面的专节。

扩展@版本 无人值守结论 用途 pi -p 新增工具 调用测试 HOME 或仓库中的额外文件 上月下载量 许可证
pi-mcp-adapter@7.0.1 空服务器列表查询完成;会改写设置 MCP mcp 列出的服务器为空 agent 目录中的 settings.json、mcp-onboarding.json 1,408,674 MIT
pi-mcp-extension@1.13.0 状态查询能用;未配置服务器 MCP mcp_status, mcp_connect, mcp_list_resources, mcp_read_resource 状态返回 [] 未观察到 823,848 MIT
@fitchmultz/pi-mcp-adapter@8.0.30 能用;不改设置 MCP mcp_search 搜索返回无工具(未配置服务器) 未观察到 5,684 MIT
pi-web-access@0.47.0 没有搜索 key 也搜到了结果 网页搜索 web_search, source_check, fetch_content, get_search_content 搜索返回 Pi 扩展目录 未观察到 1,116,299 MIT
pi-web-search@1.7.0 用当前选定的模型能搜到结果 网页搜索 web_search 搜索返回结果 未观察到 75,116 MIT
@zeldrisho/pi-web-search@0.14.5 需要 Brave key 网页搜索 web_search 报错:BRAVE_SEARCH_API_KEY is required 未观察到 1,875 MIT
pi-wsearch@0.6.0 需要搜索 key 网页搜索 WebSearch, WebFetch 「未配置搜索引擎 key」 未观察到 1,830 MIT
pi-subagents@0.84.0 名称修正后能用 子 agent subagent, contact_supervisor, bg_wait, subagent_supervisor 模型猜错 agent 名称,重试后返回 PONG agent 目录中 missions/ 下的 JSON 726,352 MIT
@tintinweb/pi-subagents@0.23.1 能用;会保存子会话 子 agent Agent, SubagentWorkflow, get_subagent_result, steer_subagent 返回 PONG agent 目录中 sessions/ 下的 JSONL 47,391 MIT
pi-ptc-subagents@7.9.3 能用 子 agent ptc_run_code, ptc_workflow 返回 PONG 未观察到 6,902 Apache-2.0
pi-subagent-manager@1.6.1 需要会话目录 子 agent agent_types, agent_spawn, agent_wait, agent_steer, agent_status, agent_update, agent_pause, agent_stop, agent_output 带 --no-session 时启动失败(ENOENT: mkdir '');给会话目录后返回结果 未观察到 8,070 MIT
@juicesharp/rpiv-ask-user-question@2.15.0 没有 UI 时不启用 向用户提问 无(没有 UI 时被移除) 模型回答 NO-TOOL 未观察到 151,853 MIT
@plannotator/pi-extension@0.28.49 没有 UI 时不启用 计划评审 无(没有 UI 时被移除) 模型回答 NO-TOOL 未观察到 89,716 MIT OR Apache-2.0
@juicesharp/rpiv-todo@2.15.0 能用 任务管理 todo 添加并列出 "write tests" 未观察到 87,414 MIT
pi-background-tasks@2.6.10 能用;会留下输出 任务管理 fusion_reason, fusion_investigate, fusion_research, fusion_validate, bg_delegate, bg_result, bg_run, bg_run_pi_attested, bg_status, bg_logs, bg_kill 返回 hi-from-bg 仓库中的 .pi/tasks/ 18,054 ISC
@gotgenes/pi-permission-system@42.0.0 需要配置 安全 无(检查 bash 调用) Bash 审批被拒绝 agent 目录中的权限审查日志 46,073 MIT
cc-safety-net@2.7.3 命令拦截生效 安全 无(检查 bash 调用) 拦下 git reset --hard ~/.cc-safety-net/logs/ 114,610 MIT
context-mode@2.1.0 能用;会保留数据库 上下文 ctx_execute, ctx_batch, ctx_index, ctx_search, ctx_resume, ctx_stats, ctx_doctor, ctx_upgrade, ctx_purge 返回 hi-from-ctx ~/.context-mode/projects/<hash>/context.db 103,931 Elastic-2.0
pi-lens@4.4.8 能用;首次调用会下载工具 代码反馈 lens_diagnostics, symbol_search, effective_config, project_report, module_report, read_symbol, read_enclosing, pi_lens_activate_tools README 诊断未报问题 ~/.pi-lens/、下载的二进制文件 86,632 MIT
pi-hermes-memory@0.10.0 能用;会保留记忆 记忆 memory_add, memory_replace, memory_remove, skill_manage, session_search, memory_search 保存了测试用的颜色 agent 目录中的 MEMORY.md、SQLite 文件 25,570 MIT

cc-safety-net 支持多个 coding agent,它的 npm 关键词包含 claude-code、opencode 和 pi-package,在 Pi 中可以作为普通扩展加载。context-mode 使用的 Elastic-2.0 允许查看源码,但不属于 OSI 认可的开源许可证。

这次 Pi 扩展怎么测

我是 Larry 🍀 Orbi,Orbi 的创始人。Pi 在 CI 或 systemd 服务里运行时,我关心模型拿到了哪些工具,扩展运行后又留下了什么。扩展版本更新很快,这些结论只适用于上表的版本和这次测试的日期。

测试环境是 Pi 1.5.0、Node v22.23.1、/usr/bin/npm 12.0.2 和 Linux 6.18。模型用 DeepSeek 的 deepseek-flash,我通过一个本地日志代理记录发给模型的工具清单。

E2 没有会话总线。我用 env -i 清空环境变量,用 setsid -w 脱离终端,stdin 接 /dev/null,HOME 和 agent 目录都是新建的。agent 目录存放 Pi 的配置和状态,默认在 ~/.pi/agent,可用 PI_CODING_AGENT_DIR 更改位置。扩展参数是 -ne -e npm:<pkg>@<version>,运行时加上 timeout 120。这组扩展参数只加载显式指定的包。环境里有模型服务商的 key,没有额外提供搜索服务商的 key。加载测试的 prompt 是 "Reply with the single word OK."。加载后,我再要求每个扩展实际完成一次操作,每次调用测试都从已装好扩展的 E2 HOME 副本开始。

E1 多了一条私有会话总线,由下方的 blackhole.py 占用 org.freedesktop.secrets,记录每次调用但永不应答。每次 E1 运行都从对应的 E2 HOME 副本开始。20 个扩展都在 E1 和 E2 中测过,每个扩展在两种环境里提供的工具和调用测试结果都相同,所以表格中每个扩展只列一行,Secret Service 调用另放在后面的专节。E1 计时包含 1 秒的总线启动时间;同一总线下不加载扩展的 E1 对照组,在 1.2 秒时发出了首次模型请求。

不加载扩展的对照组恰好只有 read,bash,edit,write 四个工具。用 busctl 调用黑洞,5 秒后超时;另一个自写扩展在加载时读取钥匙串,3 次运行中 3 次都访问到了黑洞。

无人值守跑 Pi,哪些地方会出问题

MCP 适配器改的设置会影响下次会话

即使只用一次性的 -e,pi-mcp-adapter@7.0.1 第一次加载时也写入了这份配置:

{"extensions":["-builtin:mcp"]}

它还写了 mcp-onboarding.json。Pi 的 docs/mcp.md 第 268 行说明了扩展替代内置 MCP 的情况,并记录了用这项设置禁用内置 MCP 的方法。

为了确认这项设置的影响,我在新的 agent 目录里放了下面这份 mcp.json,指向本地已安装的 @modelcontextprotocol/server-everything:

{"mcpServers":{"everything":{"command":"/var/tmp/pex/mcpsrv/node_modules/.bin/mcp-server-everything","args":["stdio"]}}}

command 里是测试机上的安装路径,照做时换成你自己装的位置。

启动时不带 -ne,Pi 的内置 MCP 就连接了这台服务器:模型拿到了 codemode 工具,Pi 也写出了 mcp.log。我把同一份 mcp.json 复制到适配器用过的 agent 目录,再次启动 Pi,仍然不带 -ne,也不加载适配器。这次 codemode 和 mcp.log 都没有出现,因为该目录的 settings.json 中仍有 -builtin:mcp,内置 MCP 依然被禁用。省略 -ne 不能恢复它。我把这份 settings.json 里的 extensions 列表清空后再跑,codemode 和 mcp.log 都回来了。这几次连接 MCP 服务器的运行只用于验证配置副作用,表格里的 MCP 调用测试没有连接任何服务器。

在服务里运行时,可以为每次会话准备独立、可写的 agent 目录,或者每次运行都重写 settings.json。

包名拼错,Pi 也可能以 0 退出

Pi 1.5.0 会打印 Warning: Extension package "...": failed to install (...),然后继续运行,退出码为 0。我用 Pi 1.2.0(/usr/bin/pi)跑了同一条命令,退出码是 1。Pi 更新日志的 1.5.0 一节记录了这项变化,对应 PR #11304。

无人值守时,拼错包名可能让运行缺少预期扩展,却不报失败,所以下方脚本会检查这条警告。如果扩展在加载时抛异常,Pi 1.5.0 仍以 1 退出。

提问和计划评审工具需要 UI

rpiv-ask-user-question 的 reconcile.ts 在 !ctx.hasUI 时移除 ask_user_question。Plannotator 的 index.ts 在同样的条件下停用工具。这次两者都顺利加载,在 pi -p 下却都没提供工具。要求模型调用时,它回答 NO-TOOL。

默认权限配置会拦住普通操作

没写配置时,pi-permission-system@42.0.0 拒绝了每一次 bash 调用尝试,连 ls -la 也一样:

This 'bash' call requires approval, but no interactive UI is available.

测试要求执行 mkdir -p scratch && rm -rf scratch && echo removed,不加载扩展的对照组执行后输出了 removed。扩展在无法获得批准时会拒绝调用。无人值守时,需要配置好任务所需的权限;这次没有测试这样的配置。

cc-safety-net@2.7.3 没加工具,用 BLOCKED by CC Safety Net 和规则 git.reset-hard 拦下了 git reset --hard。不加载扩展的对照组在临时仓库里执行了这条命令,退出码为 0。扩展把日志写到了 ~/.cc-safety-net/。

npm 跳过了安装脚本

这套环境中的 npm 12 默认阻止安装脚本:

npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:

三次安装各有一个包的脚本被阻止:Plannotator 的 node-pty、permission-system 的 tree-sitter-bash,以及 pi-lens 的 @ast-grep/cli postinstall。权限扩展仍按前文所述拒绝 bash 调用,lens_diagnostics 返回了 clean=1, unavailable=0, failed=0,重跑时还报告 Primary LSP (marksman): confirmed clean,所以这次测到的部分能运行。但这些包的其他部分是否缺了原生组件,无法由此判断。Plannotator 因为没有 UI 而不提供工具,这次测试也就无法说明跳过安装脚本会怎样影响它的行为。

状态文件不全在 agent 目录里

只改 PI_CODING_AGENT_DIR,有些状态文件仍会留在别处。Context-mode 写了 ~/.context-mode/projects/<hash>/context.db。Pi-lens 写了 ~/.pi-lens/,第一次诊断时还把 marksman 和 typos-lsp 下载到它的 bin/ 目录。第一次用了 41.2 秒;二进制文件已经在时,同一个调用测试在 E2 用了 11.3 秒,在 E1 用了 11.8 秒。

Background-tasks 在仓库的 .pi/tasks/ 留下了输出和元数据。Tintinweb 的子 agent 在 agent 目录里保存了会话,父会话带了 --no-session 也一样。Hermes-memory 在同一个 agent 目录里写了 MEMORY.md 和 SQLite 文件,多次运行共用这个目录就会沿用保存的记忆。

钥匙串测试没有复现 9 月的卡死

9 月 4 日曾发生过一次与 systemd 运行环境有关的卡死,记录见当时的事故和生产环境那篇。

没有配置 MCP 服务器时,pi-mcp-adapter@7.0.1 无论启动还是调用 mcp,都没有访问永不应答的 Secret Service。我没测 OAuth 或 bearer token 存储。

我的自写扩展在加载时通过 @napi-rs/keyring 读取钥匙串,3 次运行中 3 次都被黑洞记录到了。首次模型请求在 3.2 秒发出,对照组是 1.2 秒。两者都包含相同的 1 秒总线启动时间,首次请求时间相差约 2 秒,之后 Pi 继续运行,没有卡死。我没查明是哪一层在约 2 秒后放弃等待;busctl 对同一个黑洞设置了 5 秒超时,就等满了 5 秒。

除自写测试扩展外,20 个扩展里唯一记录到的 Secret Service 调用来自 pi-lens,发生在总线启动后的 2.3 秒,当时它正在做第一次诊断和下载二进制文件。它的首次模型请求早在 1.3 秒时就已发出(E1 对照组是 1.2 秒),因此这次调用不可能拖慢启动。自写扩展在加载时访问钥匙串,会挡在首次模型请求之前;pi-lens 在工具调用期间访问,首次请求已经发出。那次运行同时还在下载这两个二进制文件,所以我没有单独测出这次调用本身的耗时。二进制文件已经在时,E1 里同一个调用测试用了 11.8 秒,黑洞没有记录到调用。

这套测试能检测 Secret Service 访问,这次没有复现之前的卡死;上面的 3 次验证用的是我的自写钥匙串扩展。将下方黑洞服务的代码原样保存为 blackhole.py,需要 dbus-python 和 PyGObject。如果总线上已有服务占用了 org.freedesktop.secrets,脚本会以 1 退出,因为正常钥匙串替它应答会让所有测试看起来都没问题:

#!/usr/bin/env python3
"""A Secret Service that never answers: owns org.freedesktop.secrets on the
current session bus, logs every method call addressed to it, and never replies.
Usage: blackhole.py <logfile>"""
import sys, time
import dbus, dbus.bus, dbus.lowlevel, dbus.mainloop.glib
from gi.repository import GLib

dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
NAME = "org.freedesktop.secrets"
if bus.request_name(NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE) != dbus.bus.REQUEST_NAME_REPLY_PRIMARY_OWNER:
    sys.exit(f"{NAME} is already owned on this bus: a real keyring is running, nothing would be tested")
me = bus.get_unique_name()
log = open(sys.argv[1], "a", buffering=1)
log.write(f"{time.time():.3f} ready {NAME} as {me}\n")


def on_message(_conn, msg):
    if msg.get_type() == 1 and msg.get_destination() in (NAME, me):
        log.write(f"{time.time():.3f} call {msg.get_interface()}.{msg.get_member()} {msg.get_path()} from {msg.get_sender()}\n")
        return dbus.lowlevel.HANDLER_RESULT_HANDLED  # swallow: no reply ever
    return dbus.lowlevel.HANDLER_RESULT_NOT_YET_HANDLED


bus.add_message_filter(on_message)
GLib.MainLoop().run()

下方的测试脚本会清空环境变量,默认不传入总线。设置 KEEP_BUS=1 才会把当前会话总线传给 Pi;把黑洞和测试脚本一起放进私有总线里启动:

dbus-run-session -- bash -c 'rm -f calls.log; python3 blackhole.py calls.log & for i in $(seq 50); do grep -q " ready " calls.log 2>/dev/null && break; sleep 0.1; done; grep -q " ready " calls.log || exit 1; KEEP_BUS=1 ./try-pi-extension.sh npm:<pkg>@<version>'

calls.log 中每条带 call 的行,都是一次没有得到应答的 Secret Service 请求。用我的钥匙串测试扩展运行时,日志记录了一次 org.freedesktop.Secret.Service.OpenSession 调用;同一条命令去掉 KEEP_BUS=1 后没有记录到调用,因为 Pi 看不到总线。

在 Orbi 里固定选中的扩展版本

用 Orbi 运行 Pi 时,在 orbi.toml 里写:

[[pi_extensions]]
source = "npm:pi-web-access@0.47.0"

[[pi_extensions]]
source = "npm:@tintinweb/pi-subagents@0.23.1"

[[pi_extensions]]
source = "npm:@juicesharp/rpiv-todo@2.15.0"

[[pi_extensions]]
source = "npm:cc-safety-net@2.7.3"

Orbi 的 origin/main f5de8082 中,src/orbi/config.py 的校验器接受了这些来源。它拒绝了 npm:pi-web-access、npm:pi-web-access@latest 和 npm:@tintinweb/pi-subagents,报错为 "npm source must pin a version"。

Pi 1.5.0 更新日志的 1.5.0 一节提到,-e npm: 指定的包如果没有固定版本,npm 包仓库里出现更新的匹配版本时会重新安装。生产环境那篇的加载一节解释了 Orbi 怎样把配置中的扩展传给 Pi。

留下一个扩展之前,自己跑一次

把下面代码原样保存为 try-pi-extension.sh,再执行 chmod +x try-pi-extension.sh。PATH 中需要有 Pi、Node、npm、jq、setsid 和 timeout,运行前还要导出模型服务商的 key。脚本默认读取 DEEPSEEK_API_KEY,provider 为 deepseek,模型为 deepseek-flash。可以用 KEYVAR 选择其他 key 变量,通过可选参数传入 provider 和模型。请保存成脚本运行,不要直接粘贴到交互式 shell 里。

#!/usr/bin/env bash
# try-pi-extension.sh <source> [provider] [model]
# Starts Pi once the way a service would: no terminal, no desktop session bus,
# stdin closed, a throwaway HOME, only this one extension. Exits non-zero if
# Pi fails, times out, or never builds a system prompt.
set -euo pipefail
src=${1:?usage: try-pi-extension.sh npm:<package>@<version> [provider] [model]}
[ -e "$src" ] && src=$(realpath "$src")  # local file: Pi runs from another directory
provider=${2:-deepseek}
model=${3:-deepseek-flash}
keyvar=${KEYVAR:-DEEPSEEK_API_KEY}
key=${!keyvar:?export $keyvar first}
bus=()  # KEEP_BUS=1 passes the current session bus through (to test with blackhole.py)
if [ "${KEEP_BUS:-0}" = 1 ]; then
  bus=("DBUS_SESSION_BUS_ADDRESS=${DBUS_SESSION_BUS_ADDRESS:?KEEP_BUS=1 needs a session bus}")
fi
work=$(mktemp -d)
mkdir -p "$work/agent" "$work/repo"
start=$(date +%s)
set +e
(cd "$work/repo" && env -i HOME="$work" PATH="$PATH" PI_CODING_AGENT_DIR="$work/agent" "$keyvar=$key" "${bus[@]}" \
  setsid -w timeout "${TIMEOUT:-120}" pi -p --mode json --no-session \
  --provider "$provider" --model "$model" -ne -e "$src" \
  "Reply with the single word OK." < /dev/null > "$work/out.jsonl" 2> "$work/err")
rc=$?
set -e
echo "exit=$rc seconds=$(( $(date +%s) - start )) workdir=$work"
tools=$(jq -r 'select(.type=="message_start" and .message.role=="system")
  | .message.toolsAdded[]?.name' "$work/out.jsonl" 2>/dev/null | sort -u || true)
# Pi 1.5.0 skips an npm or git extension that fails to install and still exits 0.
if [ "$rc" -eq 0 ] && grep -qE 'Extension package ".*": failed to install' "$work/err"; then
  rc=3
fi
# In --mode json, a failed model request (bad key, provider down) also exits 0.
stop=$(jq -rs '[.[] | select(.type=="message_end" and .message.role=="assistant")]
  | last | .message.stopReason // "none"' "$work/out.jsonl" 2>/dev/null || echo none)
if [ "$rc" -eq 0 ] && { [ "$stop" = error ] || [ "$stop" = aborted ] || [ "$stop" = none ]; }; then
  rc=4
  jq -rs '[.[] | select(.type=="message_end" and .message.role=="assistant")]
    | last | .message.errorMessage // empty' "$work/out.jsonl" 2>/dev/null >> "$work/err" || true
fi
if [ "$rc" -ne 0 ] || [ -z "$tools" ]; then
  echo "FAILED (exit $rc: 124 timeout, 3 extension skipped, 4 model request failed); last lines of stderr:"
  tail -n 20 "$work/err"
  exit 1
fi
echo "tools the model was given:"
echo "$tools"
echo "files left behind, outside the extension's own install:"
(cd "$work" && find . -type f -not -path './agent/tmp/*' -not -path './.npm/*' \
  -not -name out.jsonl -not -name err | sort)

例如,导出 DEEPSEEK_API_KEY 后:

$ ./try-pi-extension.sh npm:pi-web-access@0.47.0
exit=0 seconds=3 workdir=/tmp/tmp.9hlvXJ1bBX
tools the model was given:
bash
edit
fetch_content
get_search_content
read
source_check
web_search
write
files left behind, outside the extension's own install:
./agent/auth.json
./agent/models-store.json

我用最终版脚本跑过下面这些失败情况。bad.ts 是一个三行扩展,Pi 加载它时就抛异常:

export default function () {
  throw new Error("boom at load");
}
$ DEEPSEEK_API_KEY=sk-invalid ./try-pi-extension.sh npm:@juicesharp/rpiv-todo@2.15.0
exit=0 seconds=1 workdir=/tmp/tmp.rRQOCMjdmz
FAILED (exit 4: 124 timeout, 3 extension skipped, 4 model request failed); last lines of stderr:
401: {"message":"Authentication Fails, Your api key: ****alid is invalid (...)","type":"authentication_error","param":null,"code":"invalid_request_error"}

$ ./try-pi-extension.sh npm:no-such-pi-extension-xyz@1.0.0
exit=0 seconds=1 workdir=/tmp/tmp.pRpGIwtQ0o
FAILED (exit 3: 124 timeout, 3 extension skipped, 4 model request failed); last lines of stderr:
Warning: Extension package "npm:no-such-pi-extension-xyz@1.0.0": failed to install (npm install no-such-pi-extension-xyz@1.0.0 --prefix /tmp/tmp.pRpGIwtQ0o/agent/tmp/extensions/npm/f35b2129 --legacy-peer-deps --no-audit --no-fund failed with code 1). Check the source and try again.

$ ./try-pi-extension.sh ./bad.ts
exit=1 seconds=0 workdir=/tmp/tmp.9PqJAU0PEu
FAILED (exit 1: 124 timeout, 3 extension skipped, 4 model request failed); last lines of stderr:
Error: Failed to load extension "/var/tmp/pex/bad.ts": Failed to load extension: boom at load
Hint: Start without extensions using "pi -ne".

$ TIMEOUT=2 ./try-pi-extension.sh npm:pi-lens@4.4.8
exit=124 seconds=2 workdir=/tmp/tmp.MZuYKgOPSZ
FAILED (exit 124: 124 timeout, 3 extension skipped, 4 model request failed); last lines of stderr:

脚本列出的是模型拿到的全部工具,包含 Pi 内置的四个工具;上面的表格只列新增工具。第一行的 exit= 是执行 Pi 命令时得到的退出状态,其中 124 来自外层的 timeout;FAILED (exit 后的数字是脚本检查后的分类,括号里的说明每次都一样,列出各分类的含义。Pi 1.5.0 在 --mode json 下,遇到错误 key 或不存在的包都以 0 退出,因此脚本还要自己检查最后一条 assistant 消息和安装警告。上面每种失败都会让脚本以 1 退出;缺少参数或 key 也一样,在 Pi 启动前就退出。./bad.ts 这样的本地路径会先转成绝对路径,因为脚本会换到临时目录里运行 Pi。

加载检查通过后,还要实际调用一次工具。提问扩展就能通过这个脚本,模型拿到的却只有对照组那四个工具。

相关

Pi 专题页收录了整个系列。Pi 扩展在生产环境里怎么用讲隔离和自写扩展;Orbi 跑 Pi跟着 runner 看运行过程和事故记录。也可以浏览 Pi 扩展目录,查看 Orbi 与 Claude Code 对比或 Orbi Cloud。Orbi 源码在 orbi-build/orbi。

每周一个真实交付

每周一封,讲一次真实交付。随时退订。