PKU harness

编写第一个插件

写一个只有一个工具的 PLUGIN.md 插件,用录制的响应测试它,再以链接的形式分享。示例是空闲教室插件的简化版。

本页带你写一个只有一个工具的小型插件文档。这个工具回答一个问题:现在各教学楼分别有多少间空闲教室。写好之后,你会离线测试它,并把它作为链接分享出去。

示例是插件仓库里真实插件 plugins/classrooms 的简化版。它只调用一个公开的校园接口,所以不需要登录。

开始之前,请装好 bun 和 git。在最后一步之前,你都不需要 App 或手机。

完整的语言说明见声明式插件,规范原文见 PLUGIN-LANGUAGE.md。

获取作者 CLI

插件仓库里有一个给插件作者用的命令行工具(CLI)。它做的检查和 App 完全一样,所以你在自己电脑上就能发现错误,不必等到有人安装时才出问题。

git clone https://github.com/pkuinfo/pku-harness-plugins
cd pku-harness-plugins
bun install

命令的格式是:bun run plugin validate|test|run <plugin-dir> [tool] [json-input]。

编写 PLUGIN.md

一个插件就是一个文件夹,PLUGIN.md 是其中的主文件。这个文件分为两部分。

顶部两行 --- 之间的 YAML frontmatter 是插件的「清单」(manifest)。它写明插件的名字、包含哪些工具、可以访问哪些网站。下面的 Markdown 正文是写给 AI 助手看的,告诉它什么时候、怎样使用你的工具。

my-classrooms/PLUGIN.md
---
spec: 1
id: my-classrooms
name: 教室概况
description: 各教学楼现在有多少空闲教室。
version: 0.1.0
author: your-github-name
network: [portal.pku.edu.cn]
tools:
  overview: tools/overview.yaml
i18n:
  en:
    name: Classroom overview
    description: How many classrooms are free in each teaching building right now.
    tools: { overview: { title: Free rooms by building, summary: Free classrooms in each building } }
---

my-classrooms__overview:各楼现在空闲教室数量。用户想找自习的地方时先用它挑楼。

network 列出插件可以访问的全部公开域名。访问其他域名的请求都会失败,而同学们在安装前看到的也正是这份列表。i18n.en 里放英文文字,使用英文界面的用户会看到它们,而不是中文。

App 只加载 frontmatter 里写明的文件,比如这里的 tools/overview.yaml。它从不列出文件夹的内容,所以插件放在任何静态文件服务器上都能用。

编写工具

一个工具文件描述一个操作。它包含给人看的字段、给模型看的字段、声明的效果,以及按顺序执行的一组步骤。

my-classrooms/tools/overview.yaml
title: 各楼空闲教室数
summary: 各教学楼现在的空闲教室数量
description: 'How many classrooms are free right now in each teaching building: building, free, total.'
effect: read
steps:
  - let: r
    request:
      url: https://portal.pku.edu.cn/publicQuery/classroomQuery/retrClassRoomInfos.do
      headers: { referer: 'https://portal.pku.edu.cn/publicQuery/', accept: 'application/json' }
    as: json
  - check: "r.success && isList(r.rows)"
    error: 教室概况查询未成功
return: |
  {
    buildings: r.rows.map(b => ({ building: b.buildingName, free: b.classroomFree, total: b.classroomSum })),
  }

各字段的作用如下:

  • title 是工具在 App 里显示的名字,用中文写;英文版写在 i18n.en.tools.overview.title。
  • summary 可以不写。它是一行不超过 80 个字符的说明,告诉用户这个工具做什么,App 会把它显示在安装审核页的工具名下方。英文版写在 i18n.en.tools.overview.summary;如果不写英文版,App 就不会给使用英文界面的用户显示这一行说明。
  • description 只给模型看,从不显示给用户。写清楚工具返回什么、什么时候该用。
  • effect(效果)取 read、write、money 或 public。用户会按效果分别决定助手能否不经询问直接运行工具,所以必须如实声明。
  • 第一个步骤发出请求,把解析后的 JSON 绑定到名字 r 上。如果响应和预期不符,check 步骤会带着错误信息停止。return 负责组装结果。

返回数据,不要返回指令。模型会把结果当作数据来读。

YAML 里表达式的引号

如果表达式或 ${…} 模板里含有 : ,或者以 {、[ 开头,就要给它加引号。不加引号时,YAML 会把它当成映射或列表,而不是字符串。遇到这种情况,validate 会给出提示。

录制测试

测试回放事先录好的响应,而不去访问真实网站。所以它可以离线运行,每次结果都一样,校园网页改版后也不受影响。把一段真实响应裁剪到需要的部分,贴进 body。如果响应很长,可以存成测试旁边的文件,再用 file: 引用。

my-classrooms/tests/overview.yaml
tool: overview
responses:
  - match: retrClassRoomInfos.do
    headers: { content-type: application/json }
    body: '{"success":true,"rows":[{"buildingName":"一教","classroomFree":16,"classroomSum":32}]}'
expect: { buildings: [{ building: 一教, free: 16, total: 32 }] }

测试用 tool 指明要运行的工具,还可以给出 input 和一个固定的时间 now。responses 里的每一项,会回应 URL 中包含 match 文字的请求,内容来自 body 或 file。如果是校园请求,还要加上 campus,写明服务名。

测试的最后写下面两个字段之一。expect 会和工具的结果做严格比较;expectError 是一个模式,错误信息必须与它匹配。

校验、测试、运行

bun run plugin validate path/to/my-classrooms
bun run plugin test path/to/my-classrooms
bun run plugin run path/to/my-classrooms overview '{}'

validate 检查清单、工具的 schema、每个表达式和每个文件路径,和 App 以及插件市场做的检查完全一样。test 回放 tests/ 里的文件。run 会真实调用工具,但只限公开域名。校园请求必须在 App 里发出,因为只有 App 掌握用户的登录状态,所以要用录制的测试覆盖它们。

分享

把文件夹推送到一个公开的 GitHub 仓库。文件夹的 URL 就是安装链接:

https://github.com/<you>/<repo>/tree/main/my-classrooms

其他同学可以在 App 里粘贴这个链接、扫描它的二维码,或者打开 pkuharness://plugin?src=<url-encoded link>。安装之前,App 会先显示审核页:标明这是社区插件还是官方插件,列出它能访问的网站(这里是 portal.pku.edu.cn),并列出每个工具及其说明和效果。

此后每一次 git push 都是一次更新。想把插件登记到插件市场,就去提交仓库。一个仓库里可以放任意多个插件。见发布。

下一步

  • 输入、步骤、表达式、宿主能力和错误处理:声明式插件
  • 在 App 里和助手一起起草、校验、试运行插件:让 AI 帮你写插件
  • 需要用到同学校园登录状态的请求:声明 campus(见规范 §4 和 §9)
  • 审核会检查什么:审核与安全

本页目录