PKU harness
Build a plugin

Porting from pkucli

pkucli is the protocol reference for PKU services. How to port endpoints, login steps and quirks from its Rust crates into a plugin, leave sign-in to the app, and credit the source file.

pkuinfo/pkucli is a set of command-line tools for PKU web services, written in Rust. Its authors have already worked out the hard parts of each service: which endpoints exist, which sign-in steps the service needs, which headers and parameters it requires, and how its HTML is structured.

When you write a plugin for a service that pkucli already supports, port pkucli's behaviour instead of working it out again, and cite the source file in a comment. This page shows where to find things in pkucli, which parts belong in the plugin and which stay in the app, and an example.

Most services sign in through IAAA, PKU's central sign-in system (iaaa.pku.edu.cn). After IAAA, each service usually needs one or more single sign-on (SSO) steps of its own. We call each such step a login hop.

Where things are

pkucli crateServiceIn PKU harness
commonIAAA login (password, QR code), TOTP codes, captcha, sessions, credentialsThe app (src/core/campus); not in plugins
campuscardCampus card (bdcard)wallet plugin, card campus service
treeholeTreehole (树洞)treehole plugin, treehole service
courseBlackboard (教学网)courses plugin, blackboard service
electiveCourse selectionelective plugin, elective service
cwfw, cwpayFinance, finance paymentsfinance plugin, cwfw / cwpay services
bdkjRoom bookingroom-booking plugin, bdkj service
portalPortalThe portal's SSO hop, inside the app
claspiderCourse recordings (智云课堂)Planned
info-spider, info-authWeChat articlesnotices plugin reads single articles (cleaning ported from info-spider)

TOTP (time-based one-time password) is the six-digit code an authenticator app shows. Inside a crate, api.rs (or client.rs) holds the endpoints and response types, login.rs holds the service's own login hops, and commands.rs holds the operations a user runs.

What moves where

Each file of a pkucli crate maps to a place in PKU harness. Sign-in moves into the app, and everything else moves into the plugin:

pkucli crate                     PKU harness
─────────────────────────────    ──────────────────────────────────────────────
login.rs  (IAAA → service SSO)   app: a campus service adapter (harness-owned)
client.rs (headers, base URL)    plugin: the `url`/`headers` of services.campus.request
api.rs    (endpoints, types)     plugin: src/api.ts + zod/TS types
commands.rs / display.rs         plugin: tools (data, not formatted text)
  • Sign-in stays in the app. A plugin never implements IAAA or a service's SSO hops, so it never handles a password. Today the app signs in to card, its, treehole, blackboard, bdkj, cwfw, cwpay, elective, library and library-space. If the service you need is not in that list, open an issue in pku-harness. A new service needs a new adapter in src/core/campus/adapters/.
  • Requests go through services.campus.request(service, req). Give paths relative to the service's host. The app adds the cookies or tokens.
  • Public pages that need no login can use plain fetch with requires: ['network']. Copy pkucli's headers, such as a desktop user agent and accept-language: zh-CN, because some campus servers reject requests without them.
  • Parse into data. pkucli's display.rs formats output for a terminal. A tool returns structured JSON instead, because its reader is the model or a screen.

Example

Here is the campus card query in pkucli, written in Rust:

crates/campuscard/src/api.rs
/// 查询校园卡信息
pub async fn query_card(&self) -> Result<CardQueryData> {
    let url = format!("{CARD_BASE}/berserker-app/ykt/tsm/queryCard?synAccessSource=h5");
    let resp = self.http.get(&url).send().await.context("查询校园卡失败")?;
    let api_resp: ApiResp<CardQueryData> = parse_response(resp).await?;
    check_resp(api_resp)
}

The same request in a plugin, written in TypeScript. The comment at the top credits the pkucli file and records two quirks: amounts are in fen (0.01 yuan), and every call carries synAccessSource=h5.

plugins/shared/campus-card.ts (abridged)
/**
 * Campus card (bdcard.pku.edu.cn) response handling, ported from pkucli
 * crates/campuscard/src/api.rs. Amounts are in fen; every call carries synAccessSource=h5.
 */
import type { HostServices } from '@pku-harness/sdk';

type Services = Pick<HostServices, 'campus'>;
const get = (services: Services, url: string) => services.campus.request('card', { url });

export async function cardAccount(services: Services) {
  const data = unwrap<{ card: CardInfo[] }>(await get(services, '/berserker-app/ykt/tsm/queryCard?synAccessSource=h5'));
  const account = data.card[0]?.account;
  if (!account) throw new Error('校园卡缺少账户信息');
  return account;
}

Second factors

Some services ask for an extra check, such as an SMS code. pkucli asks for the code in the terminal, which a plugin can't do. In a plugin, a check at the service level becomes a tool result such as { status: 'verification_required', methods: ['sms'], message }, together with a tool that sends or redeems the code. Treehole's SMS check (error 40002) and its course token (error 40077) work this way. The plugin's screen can also offer services.sms.awaitCode(), which reads the code from the SMS.

Second factors at the IAAA level are handled by the app, not by plugins.

Rules

  • MUST cite the pkucli file you ported from in a comment at the top of the module.
  • MUST keep sign-in in the app. Plugins only make requests.
  • SHOULD port each parser together with a test and a fixture made from a real, anonymised response.
  • SHOULD fix bugs in both places. If you find a quirk pkucli doesn't handle, open an issue or a pull request in pkucli too.

On this page