Your first plugin
Write a PLUGIN.md plugin with one tool, test it against a recorded response, and share it as a link. The example is a cut-down version of the free-classrooms plugin.
On this page you write a small plugin document with one tool. The tool answers one question: how many classrooms are free in each teaching building right now. You then test it offline and share it as a link.
The example is a cut-down version of a real plugin,
plugins/classrooms
in the plugins repository. It calls one public campus endpoint, so it needs no login.
Before you start, install bun and git. You don't need the app or a phone until the last step.
For the full language, see Declarative plugins. The
specification itself is
PLUGIN-LANGUAGE.md.
Get the author CLI
The plugins repository contains a command-line tool (CLI) for plugin authors. It runs the same checks as the app, so you can find mistakes on your computer before anyone installs the plugin.
git clone https://github.com/pkuinfo/pku-harness-plugins
cd pku-harness-plugins
bun installThe command has this form: bun run plugin validate|test|run <plugin-dir> [tool] [json-input].
Write PLUGIN.md
A plugin is one folder, and PLUGIN.md is its main file. The file has two parts.
The YAML frontmatter at the top, between the --- lines, is the manifest. It names the plugin,
lists its tools and declares which websites it may reach. The Markdown body below it is written for
the AI assistant. It tells the assistant when and how to use your tools.
---
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 lists every public host the plugin may call. A request to any other host fails, so this
list is also what students see before they install. i18n.en holds the English text that
English-speaking users see instead of the Chinese.
The app loads only the files the frontmatter names, such as tools/overview.yaml here. It never
lists the folder's contents, so you can host the plugin on any static file server.
Write the tool
A tool file describes one operation. It has fields for people, a field for the model, a declared effect, and a list of steps that run in order.
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 })),
}The fields work as follows:
titleis the tool's name in the app. Write it in Chinese; the English version goes ini18n.en.tools.overview.title.summaryis optional. It is one line of at most 80 characters that tells people what the tool does, and the app shows it under the title on the install review sheet. Its English version goes ini18n.en.tools.overview.summary. If you leave that out, the app shows English users no summary line.descriptionis for the model only and is never shown to users. Say what the tool returns and when to use it.effectisread,write,moneyorpublic. Users decide for each effect whether the assistant may run a tool without asking, so the effect must be honest.- The first step sends the request and binds the parsed JSON to the name
r. Thecheckstep stops with an error message if the response is not what you expect.returnbuilds the result.
Return data, never instructions. The model reads the result as data.
Quoting expressions in YAML
Put quotes around any expression or ${…} template that contains : or starts with { or [.
Without quotes, YAML reads it as a map or a list instead of a string. validate prints a hint
when this happens.
Record a test
A test replays a recorded response instead of calling the real website. It therefore runs offline
and gives the same result every time, even after the campus page changes. Paste a real response,
trimmed to the parts you need, as the body. For a long response, save it as a file next to the
test and refer to it with file:.
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 }] }A test names the tool to run. It may also give an input and a fixed time, now. Under
responses, each entry is served to any request whose URL contains the match text, and its
content comes from body or file. For a campus request, also add campus with the service name.
Finish the test with one of two fields. expect is compared exactly with the tool's result.
expectError is a pattern that the error message must match.
Validate, test and run
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 checks the manifest, the tool schemas, every expression and every file path. These are
the same checks the app and the marketplace run. test replays the files in tests/. run
calls the tool for real, but only for public hosts. Campus requests need the app, because only the
app holds the user's sign-in, so cover them with recorded tests.
Share it
Push the folder to a public GitHub repository. The folder's URL is the install link:
https://github.com/<you>/<repo>/tree/main/my-classroomsOther students can paste the link in the app, scan its QR code, or open
pkuharness://plugin?src=<url-encoded link>. Before anything is installed, the app shows a review
sheet. It says whether the plugin is community or official, lists the websites it can reach (here
portal.pku.edu.cn), and shows each tool with its summary and effect.
After that, every git push becomes an update. To list the plugin in the marketplace,
submit the repository. One repository can hold any number of plugins. See
Publishing.
Next
- Inputs, steps, expressions, host abilities and errors: Declarative plugins
- Draft, validate and try a plugin together with the assistant in the app: Write a plugin with the AI
- Requests that need the student's campus session: declare
campus(see the spec, §4 and §9) - What reviewers check: Review and safety
Build a plugin
How to choose between a PLUGIN.md document, a remote plugin and a bundled TypeScript plugin, which repositories to clone, and the order of work from your first tool to a marketplace listing.
Declarative plugins (PLUGIN.md)
The plugin language. A plugin is a folder with a PLUGIN.md manifest, instructions for the assistant, tools written as YAML steps, and generative-UI widgets. The app interprets it and installs it from a link, with no code to build and no app release.