PKU harness

插件

插件有三种形式。插件文档(PLUGIN.md)由 App 解释执行,通过链接安装;内置 TypeScript 插件随 App 发布;远程插件是通过 URL 添加的 MCP 服务器。三者共用同一个注册表、同一套权限系统和同一个 Agent 循环。

在 PKU harness 里,每一项连接校园服务的功能都由一个插件(plugin)提供。插件有三种形式。最常见的是插件文档:一个包含 PLUGIN.md 文件的文件夹,App 读取并按照其中的说明执行。另外两种分别是内置在 App 中的 TypeScript 插件,以及运行在服务器上、通过 MCP(Model Context Protocol,模型上下文协议)连接的远程插件。

助手、权限系统和插件市场用同样的方式对待这三种插件。内置插件和社区插件使用同一套权限系统。

插件的三种形式

插件文档

插件文档是一个文件夹。文件夹里有一份 YAML 清单、一段写给助手的说明,以及用 YAML 步骤写成的工具。如果插件有小组件,小组件用生成式界面的 JSON 来描述。

插件文档在手机上运行,由 App 解释执行。整个过程中不会运行任何插件代码。

它通过一个链接发布,可以是 GitHub 文件夹,也可以是任意一个 PLUGIN.md 的 URL。要更新插件,只需要 git push。官方的插件文档也随 App 内置,同样从各自的链接更新。

插件文档可以访问它声明过的校园服务(使用用户的会话)和公开网站,也可以使用宿主能力。它能提供小组件、页面、设置项和快捷指令。

内置 TypeScript 插件

内置插件是一个基于 @pku-harness/sdk 的 TypeScript 模块,用 definePlugin 定义。它运行在手机上的 App 内部,随 App 的新版本或签名的空中更新(OTA)一起发布。

内置插件可以使用宿主服务、原生界面和 PKU harness Cloud。原生界面包括页面、小组件、设置项、快捷指令和标签页。

远程插件

远程插件是一个通过 Streamable HTTP 通信的 MCP 服务器,可以用任何语言编写。它运行在你自己控制的 HTTPS 服务器上。

发布远程插件时,你部署自己的服务器。用户可以添加它的 URL,也可以从插件市场安装。远程插件只能提供工具,它的结果会作为数据交给模型。

三种形式的对比

插件文档内置插件远程插件
运行位置手机,由 App 解释执行手机,App 内部你的 HTTPS 服务器
发布方式一个链接随 App 发布部署服务器
能否看到校园账号密码不能不能不能
工具 id<plugin id>.<tool><plugin id>.<tool>mcp-<server name>.<tool>
工具 id 示例classrooms.freewallet.cardBalancemcp-demo-canteen.menu

三种插件都看不到校园账号密码。插件文档的校园请求由框架代为签发;内置插件只能通过 services.campus.request 拿到响应;远程插件根本没有校园会话。

信任级别

插件文档分为官方和社区两类。官方插件文档随 App 内置,或者来自 pkuinfo/pku-harness-plugins 仓库。社区插件的工具在执行 write、money 或 public 效果之前会先询问你,它们返回的结果也被视为不可信的内容。

内置插件都在插件仓库中经过审核。

远程插件属于第三方。默认情况下,会修改数据的工具都要先询问你。远程插件返回的任何结果都会「污染」(taint)当前这一轮对话。这意味着什么,请看安全模型。

为什么大多数插件是文档

应用商店禁止 App 下载可执行代码。插件文档是数据,不是代码,所以社区插件可以以文档的形式在手机上运行,也可以通过 MCP 在远程运行。

官方的校园服务插件 classrooms、campus-events、grades、canteen、shuttle 和 notices 都是插件文档。TypeScript 只用于框架自己的功能,例如界面、看板、定时任务、日程、待办、记忆和模型服务商,以及插件语言暂时还无法表达的服务。

如何选择插件的形式

插件只需要请求校园网站或公开网站,再从响应里提取数据吗?(需要时可以带上用户的校园会话)
├─ 是 → 插件文档(PLUGIN.md)。从快速上手开始,或者让 App 里的 AI 帮你写。
└─ 否 → 需要通用代码,或者需要你自己的服务器吗?
         ├─ 是 → 远程插件。从 templates/remote-plugin 开始。
         └─ 否,需要设备能力,或插件语言做不到的原生界面
              → 内置 TypeScript 插件(请先开一个 issue),或者提议新增一项宿主能力。

大多数校园服务只需要发请求、再从响应里提取数据,这类插件写成插件文档就够了。你可以从快速上手开始,也可以直接让 App 里的 AI 帮你写。

如果需要通用代码或自己的服务器,就写远程插件,从 templates/remote-plugin 模板开始。只有需要插件语言还不支持的设备能力或原生界面时,才写内置 TypeScript 插件。这种情况请先开一个 issue。你也可以提议新增一项宿主能力,让插件文档也能做到。

插件在 App 中的生命周期

一个插件从安装到使用,会经过下面五个阶段。

  1. 注册。 内置插件和内置的插件文档在 App 启动时注册。通过链接安装的插件文档,要等你在审查页上确认后才注册,它的文件和内容哈希会保存在手机上。远程插件在你安装时注册:App 连接服务器,列出它的工具,并缓存它的清单。
  2. 可用。 手机满足插件在 requires 中声明的能力需求时,插件才可用。不可用的插件对助手隐藏,设置页会说明原因。
  3. 启用。 插件默认是开启的。你可以关闭任何非核心插件。核心插件(标记为 core: true,例如生成式界面和记忆)不能关闭。
  4. 调用。 每次调用都走同一条路径。App 先用 zod 校验输入,再检查这个效果对应的授权方式,必要时请你批准,然后执行工具并返回结果。
  5. 更新。 这一步只适用于插件文档。App 每天最多重新拉取一次插件的链接。如果新内容的声明没有变化,就直接生效;如果声明的权限变宽了,要等你确认后才生效。详见声明式插件 → 更新。

远程插件的细节

添加。 你,或者经你批准的助手,可以添加一个 MCP Streamable HTTP 地址,并可选地附上一个 bearer token。这个 token 保存在密钥库里。

插件 id。 远程插件的 id 由 mcp- 加上 MCP 服务器的 name 组成,name 取自 initialize 响应,并会被转换成 slug 形式。请用你在注册表里的 id 作为服务器名,这样插件市场才能把两者对应起来。

工具。 每个 MCP 工具都会变成注册表里的一个工具,它的输入 schema 会从 JSON Schema 转换成 zod。标记了 annotations.readOnlyHint: true 的工具是 read 工具,其余工具都是 write 工具。

默认授权。 对于远程插件,read 工具自动执行,write、money 和 public 工具要先询问你。

结果。 结果会被包装成 { source, data }。其中 data 取自 structuredContent,或者把文本解析成 JSON。系统提示词里有一段关于这个插件的说明,它告诉模型:这些结果是数据,不能当作指令执行。

工具名。 MCP 工具名会被规整为只含 [a-zA-Z0-9_-] 的形式,其他字符会变成 _,长度会截断到 40 个字符。所以请使用简短的 ASCII 驼峰命名。

合并与重命名插件

PluginDef.legacy 字段让一个插件接管旧插件的 id。例如,wallet 插件取代了 campus-card、netfee 和 utilities 三个旧插件。

设置了这个字段之后,宿主会把旧的插件 id、工具 id 和小组件 key 映射到新的上面,并把你原来的设置迁移一次。详见 SDK → plugin。

继续阅读

本页目录