Campus sessions and IAAA
The harness keeps the campus credentials, signs in to each PKU service itself, and gives plugins the responses through `services.campus.request`. Plugins never see passwords or session tokens.
PKU services are all signed in through IAAA (统一身份认证), the university's central login system. After that, each service keeps its session in its own way. Some use cookies, some need a single sign-on (SSO) hop through the portal, and some use a JWT.
The harness implements all of this sign-in logic once, inside the app, following the protocol
reference pkucli. A bundled plugin doesn't deal with
sign-in at all. It sends a request by service name, the harness sends it with the signed-in
session attached, and the plugin receives the response. For example, to look up the campus-card
balance:
const res = await ctx.services.campus.request('card', {
url: '/berserker-app/ykt/tsm/queryCard?synAccessSource=h5',
});
// res: { status, url, headers, body }Services the harness signs in to
The table lists the four main services and how the harness builds a session for each.
| Service | Host | Session the harness builds |
|---|---|---|
card | bdcard.pku.edu.cn | IAAA → portal ssoLogin → redirectToCard → synjones JWT |
its | its.pku.edu.cn | ITS web login (网络账号, the network account) |
treehole | treehole.pku.edu.cn /chapi/… | IAAA "PKU Helper" → JWT + device uuid |
blackboard | course.pku.edu.cn (教学网) | IAAA → campusLogin cookie session |
The harness also handles expired sessions. For card it refreshes the JWT on a 401 response.
For treehole it signs in again on a 401. For blackboard it follows redirects automatically.
Adapters in the app (src/core/campus/adapters/) register more services: bdkj (room booking),
cwfw (finance), cwpay (finance payments), elective (course selection), library (the
library) and library-space (library space booking: discussion rooms and study seats).
CampusService is typed as 'card' | 'its' | 'treehole' | 'blackboard' | (string & {}). A request
for a service that isn't registered is rejected. Declare the services you use in the plugin's
scopes, so that users can see them on the plugin's page.
The request format
The second argument of request is a CampusRequest object.
type CampusRequest = {
url: string; // absolute, or a path on the service's host
method?: 'GET' | 'POST';
headers?: Record<string, string>;
body?: string;
contentType?: string;
responseType?: 'text' | 'base64';
form?: Record<string, string>; // url-encoded form, instead of body
authIn?: 'headers' | 'form'; // put the session credential in the form too (card order API)
sign?: (fields) => fields | Promise<fields>; // add a signature over the final form fields
followRedirects?: boolean; // false = read `location` yourself
multipart?: CampusMultipart; // file uploads; bytes never pass through the plugin
timeoutMs?: number; // default 20 s, 120 s for multipart
};Requests are sent by the app's native campus HTTP module, which works as follows.
- It keeps a separate cookie jar for each session.
- It lets you control redirects by hand.
- It uses HTTP/1.1.
- It includes the extra root certificates that some campus sites need.
- It uses no proxy, so sites that only work on campus are called directly.
Every type is listed in SDK → campus.
Second factors
There are two kinds of second factor. The harness handles one kind, and plugins handle the other.
The harness handles IAAA's second factors: SMS, email and the mobile token (手机令牌). Any sign-in can show a prompt, including a session refresh in the background. The request then pauses until the user completes the check.
Checks added by a service itself are left to the plugin, because only the plugin knows that service's API. Treehole, for example, returns two codes that need a check:
40002means an SMS check is needed, using/chapi/api/jwt_send_msgand/chapi/api/jwt_msg_verify;40077means a course-data token is needed.
The plugin turns these codes into a result such as
{ status: 'verification_required', methods: ['sms'], message }. The user then completes the
check, or gives the code to the agent so that the agent can complete it. On Android,
services.sms.awaitCode() offers one-tap reading of the SMS code.
If the user has bound the app's authenticator, services.campus.otp() returns the current
mobile-token code, but never the token's secret. Use it for checks such as Treehole's course
token.
Handing the user to a campus page
Sometimes a plugin needs the user to finish something in a browser. The most common case is paying for an order that the plugin created.
services.campus.openSignedIn(service) opens the service in the system browser, already signed
in. It uses the service's own hand-off mechanism. The harness builds the URL, so the plugin never
sees the token in it.
services.browser.open(url) opens any URL, such as a payment cashier page. If the URL is an app
link such as weixin:// or alipays://, the system opens that app.
Rules
- You must declare
requires: ['campus-session']. If you reach sites that only work on campus, add'campus-network'as well. - You must not ask the user for their campus password, whether in a screen, a tool input or a remote plugin. Remote plugins have no campus session today. Granting them limited sessions is on the roadmap.
- Where
pkuclialready implements an endpoint or a quirk, you must port it and cite the source file in a comment. See Porting from pkucli. - You should check
services.campus.status().signedInfirst. If the user is not signed in, return a clear message such as "请先在设置里登录校园账号" (please sign in to your campus account in Settings) instead of a raw error.
Scheduler and notifications
One scheduler runs reminders, to-dos, class reminders and scheduled checks. Each job pairs a trigger (once, or a series with an end) with an action (notify, check or agent), and fires on the minute.
Security model
Credentials stay on the phone. Tool results are data, never instructions, and third-party content taints the agent's turn. Anything that leaves the phone is declared and agreed to first.