Pi Agent 权限模式与 Thinking Effort 自定义

背景

Pi Agent 安装 @georgedong32/permission-modespi-zentui 后出现两个定制需求:

  1. 保留 Zentui 的 Starship Footer,同时显示当前权限模式。
  2. 通过 /effort 命令查看或调整模型的 Thinking 强度。

相关环境:

  • Pi:0.84.2
  • Permission Modes:@georgedong32/permission-modes@2.6.3
  • UI:pi-zentui

对 Pi Agent 插件机制的理解

Extension 是运行时模块

Pi 的插件主体称为 Extension,本质上是由 Pi 直接加载的 TypeScript 模块。模块默认导出一个工厂函数,并通过传入的 ExtensionAPI 注册能力:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function myExtension(pi: ExtensionAPI): void {
  pi.registerCommand("hello", {
    description: "示例命令",
    handler: async (_args, ctx) => {
      ctx.ui.notify("Hello", "info");
    },
  });
}

常见扩展点包括:

API用途
pi.registerCommand()注册 /command 命令
pi.registerTool()注册可由模型调用的工具
pi.registerShortcut()注册交互式快捷键
pi.on()监听会话、模型、工具调用等生命周期事件
ctx.ui.setStatus()发布可组合的状态信息
ctx.ui.setFooter()替换整个 Footer
pi.setThinkingLevel()调整当前模型的 Thinking 等级
pi.setActiveTools()动态启用或禁用工具

Extension 使用当前 Pi 进程的系统用户权限运行,因此可以访问文件、网络和进程。它不是安全沙箱,第三方 Extension 安装前需要审查来源和实现。

加载位置与作用域

常用加载位置:

位置作用域
~/.pi/agent/extensions/*.ts当前用户的所有项目
.pi/extensions/*.ts当前项目,受 Project Trust 控制
~/.pi/agent/settings.jsonpackages全局 npm 或 Git Pi Package
.pi/settings.jsonpackages项目级 Pi Package

npm Package 可通过以下命令管理:

1
2
3
4
pi install npm:<package>
pi list
pi update --extensions
pi remove npm:<package>

本次两个自定义功能放在 ~/.pi/agent/extensions/,原因是它们属于个人工作流,并且不应随某个项目仓库共享。

注册阶段与生命周期

Extension 工厂函数负责注册,不适合直接启动长期定时器或后台资源。需要长期运行的逻辑应在 session_start 中启动,并在 session_shutdown 中释放:

1
2
3
4
5
6
7
pi.on("session_start", (_event, ctx) => {
  // 初始化当前会话资源
});

pi.on("session_shutdown", () => {
  // 清理 timer、socket、watcher 等资源
});

执行 /reload 时,Pi 会先触发旧 Extension 实例的 session_shutdown,重新加载 Extension、Skills、Prompts、Theme 和上下文文件,再触发新实例的 session_start。因此 /reload 是修改本地 Extension 后的正确生效方式。

会话切换、Fork 和 Reload 后,旧的 ctx 及其会话对象会失效,不能被后台回调继续持有。本次桥接定时器在 session_shutdown 中清理,就是为了避免使用失效上下文。

UI 能力的组合方式

Pi 的 UI API 并不都具备相同的组合语义:

  • setStatus(key, value) 使用带键状态,多个 Extension 可以分别发布状态,适合与 Zentui 等聚合 Footer 配合。
  • setFooter(factory) 替换整个 Footer,同一时刻只能有一个实际拥有者;多个 Extension 调用时会出现后写覆盖前写。
  • 自定义 Editor、Working Line 等全局 UI 能力也可能存在所有权竞争,需要查看插件是否支持包装已有组件或显式交接所有权。

因此,本次没有让桥接 Extension 再调用 setFooter(),而是让它发布 permission-mode 状态,由 Zentui 统一渲染 Footer。这比两个插件争夺 Footer 更符合组合式设计。

加载顺序与冲突

多个 Extension 修改同一个全局 UI 资源时,加载顺序会影响结果。本次将 Zentui 放在 packages 数组末尾,使它最后安装并拥有 Footer。

快捷键还存在额外的保留规则。Pi 0.84.2Shift+Tab 保留给内置的 app.thinking.cycle,因此 permission-modes 注册的同名快捷键会被跳过,而不是覆盖内置行为。解决这类冲突时,应通过 ~/.pi/agent/keybindings.json 重新绑定内置动作,为 Extension 释放按键,而不是依赖插件加载顺序。

工具权限拦截通常通过 tool_call 事件实现:Extension 可以检查参数、修改输入或返回 { block: true }。这属于 Pi 进程内的策略层;即使策略实现了 allow / ask / deny,也不等同于 Bubblewrap、容器或虚拟机提供的 OS 级隔离。

本次定制体现的设计原则

  1. 一个 Extension 一个职责:Footer 桥接与 /effort 命令分别放在独立文件。
  2. 优先组合而不是覆盖:使用 setStatus() 接入 Zentui,而不是创建第二个 Footer。
  3. 不修改依赖源码:避免 pi update --extensions 覆盖 node_modules 中的手工修改。
  4. 正确管理生命周期:会话开始时初始化,结束时清理定时器。
  5. 验证实际生效值:设置 Thinking 后通过 getThinkingLevel() 检查模型最终采用的等级。
  6. 区分策略与隔离:Permission Extension 是授权判断层,不应被误认为系统安全边界。

问题原因

permission-modes 和 Zentui 都会调用 ctx.ui.setFooter()。Pi 同一时间只能使用一个自定义 Footer,因此后加载的 Extension 会覆盖先加载的 Extension。

permission-modes 当前还会清除名为 modes 的 Extension Status,无法直接利用 Zentui 已有的第三方状态展示能力。

处理方案

创建独立桥接 Extension:

1
~/.pi/agent/extensions/permission-mode-zentui.ts

桥接逻辑:

  1. 读取 permission-modes 发布的环境变量 PERMISSION_MODES_INHERITED_MODE
  2. askplanautobypass 映射为可读标签。
  3. 通过 ctx.ui.setStatus("permission-mode", ...) 发布状态。
  4. 每 200 毫秒检查一次模式,仅在模式变化时更新状态。
  5. session_shutdown 时清理定时器,避免资源泄漏。

状态标签:

模式Footer 标签
ask● Ask
plan⏸ Plan
auto▶ Auto
bypass⚡ Bypass

同时调整全局包加载顺序,确保 Zentui 最后安装 Footer:

1
2
3
4
5
6
7
{
  "packages": [
    "npm:pi-cc-extensions",
    "npm:@georgedong32/permission-modes",
    "npm:pi-zentui"
  ]
}

配置文件位置:

1
~/.pi/agent/settings.json

Zentui 会将 permission-mode 视为第三方 Extension Status。显示位置可在 /zentuiExtensions 页面中设置为左侧、中间或右侧。

方案特点

  • 不直接修改 npm 包中的 node_modules
  • 执行 pi update --extensions 后不会因包更新而丢失。
  • Permission Modes 继续负责权限控制,Zentui 独占 Footer 渲染。
  • 桥接 Extension 只负责状态同步,职责独立。

自定义二:增加 /effort 命令

文件位置

创建独立 Extension:

1
~/.pi/agent/extensions/effort-command.ts

该 Extension 只负责注册 /effort,不与 Permission Mode 或 Zentui 桥接逻辑混合。

支持的 Thinking 等级

1
2
3
4
5
6
7
off
minimal
low
medium
high
xhigh
max

使用方法

不带参数时打开选择菜单:

1
/effort

直接指定等级:

1
2
3
4
5
6
7
/effort off
/effort minimal
/effort low
/effort medium
/effort high
/effort xhigh
/effort max

命令还提供参数补全,并验证输入是否合法。设置后通过 pi.getThinkingLevel() 读取实际生效等级;如果当前模型不支持请求的等级并发生降级,会显示实际应用值。

应用配置

创建或修改 Extension 后,应使用独立的重载命令:

1
/reload

/settings reload 不是有效的重载方式;/settings/reload 是两个独立命令。

验证结果

已完成以下静态验证:

  • permission-mode-zentui.ts 通过 Node TypeScript 语法检查。
  • effort-command.ts 通过 Node TypeScript 语法检查。
  • ~/.pi/agent/settings.json 可以被正常解析为 JSON。
  • 包加载顺序已调整为 Permission Modes 在前、Zentui 在后。

实际 Footer 展示和 /effort 交互需要在当前 Pi 会话执行 /reload 后确认。

注意事项

  • Permission Modes 的 Auto 模式依赖分类模型判断风险;分类模型不可用时,写入可能 fail-closed。需要维护全局配置时可先切换到 /ask,再明确批准目标操作。
  • bypass 模式会放行高风险操作,不应作为日常默认模式。
  • 桥接 Extension 依赖 PERMISSION_MODES_INHERITED_MODE。如果 Permission Modes 后续版本变更或移除该环境变量,需要同步调整桥接实现。
  • Pi Extension 与第三方包以当前用户权限运行,不属于 OS 级安全沙箱。
使用 Hugo 构建
主题 StackJimmy 设计