PKU harness

宿主服务

内置插件需要做自身以外的事情时,都要通过 `HostServices`。框架在工具里以 `ctx.services`、在页面和小组件里以 `props.services` 的形式把它传给插件。

内置插件从不导入 App 的代码。插件需要的系统功能,例如发送校园请求或创建定时任务,由 App 提供。我们把 App 提供的这些功能叫作宿主服务。

框架把宿主服务放在一个 HostServices 对象里交给插件。在工具里,它是 ctx.services;在页面和小组件里,它是 props.services。

此外,每个插件还有自己的键值存储(ctx.kv / props.kv)。如果插件声明了云服务,它还会拿到一个云客户端(ctx.cloud)。

服务一览

服务提供什么
campus已登录的校园 HTTP 请求和登录状态
scheduler定时任务的创建、更新、删除和列出
widgets看板小组件的管理
todos待办,可以附带提醒
timetable课程、学期和上课提醒设置
notify提醒能否准时触发,以及修复入口
sms一键读取短信验证码
files系统的文件选择器,以及对话中附件的文字(列出、读取、搜索)
settingsApp 和插件的全部设置:列出、修改、打开对应页面
phoneCopilot 的手机会话(供内置的 Copilot 插件使用)
browser在系统浏览器中打开网址
cryptosha256(text)
registry已安装的插件和工具(只读)
remote远程 MCP 插件的安装、卸载和检查
genui生成式界面的组件目录和描述校验
ongoing?Android 前台服务和常驻通知

每个服务的完整签名见 SDK → HostServices。下面补充一些使用时要注意的地方。

  • campus 的主要方法是 request(service, req)。它还能返回当前的手机令牌验证码,并能在保持登录状态的情况下跳转到浏览器。使用它需要 campus-session 能力,详见校园会话。
  • scheduler 用于精确的通知和定时检查,也可以立即运行一次检查,详见定时任务。
  • widgets 可以列出、放置、移除、排序、预览和解析小组件,并提供组件目录,详见看板。
  • notify 的 fix 方法只能在用户点击时调用。
  • sms 的 awaitCode() 使用 Android 的 SMS User Consent 接口,不需要短信权限。
  • files 的 pick 只能由用户发起,绝不能在助手调用的工具里使用。list、read 和 search 读取用户在当前对话里附上的文档(PDF、Word、Excel、PowerPoint、录音转写等),内容是手机上提取出的文字。这些文字属于第三方内容,返回它们的工具要标记为 untrusted。
  • settings 和设置页面读写同一份设置。change 会拒绝未知的键和 uiOnly 的键;插件自己的设置项要在 PluginDef.preferences 中声明,见架构 → 助手能操作一切。
  • phone 驱动 Copilot 的独立屏幕。它只供内置的 Copilot 插件使用,需要 phone-control 能力。
  • browser 用于打开收银台和文档,以及框架无法代为完成的登录。
  • crypto 存在的原因是插件自己没有加密库。
  • remote 供 plugins 插件使用。
  • ongoing 只在手机具备 ongoing 能力时存在,所以它的类型带有问号。

私有存储:kv

interface KV {
  get<T>(key: string): Promise<T | undefined>;
  set<T>(key: string, value: T): Promise<void>;
  delete(key: string): Promise<void>;
}

每个插件的 kv 都有独立的命名空间。数据以 JSON 的形式存在手机上的 SQLite 数据库里。

kv 不是密钥库。请不要在里面存放用户输入的机密信息,也不要存放大块数据。

示例:一个用到三项服务的工具

下面这个工具会创建一个定时检查:每天早上 8 点查一次校园卡余额,余额低于指定金额时提醒你。它用到了 campus、scheduler 和 kv。

import { defineTool } from '@pku-harness/sdk';
import { z } from 'zod';

export const remindLowBalance = defineTool({
  title: '余额提醒',
  description: 'Every morning at 08:00, notify when the campus card balance is below `below` yuan.',
  input: z.object({ below: z.number().positive() }),
  effect: 'write',
  execute: async ({ below }, ctx) => {
    if (!ctx.services.campus.status().signedIn) throw new Error('请先在设置里登录校园账号');
    const job = await ctx.services.scheduler.create({
      title: '校园卡余额提醒',
      source: ctx.actor === 'agent' ? 'agent' : 'user',
      trigger: { type: 'every', unit: 'day', interval: 1, times: ['08:00'], startAt: Date.now() },
      action: {
        type: 'check',
        tool: 'wallet.cardBalance',
        condition: { path: 'balance', op: '<', value: below },
        title: '校园卡余额只剩 ¥{value}',
      },
    });
    await ctx.kv.set('lowBalanceJob', job.id);
    return { jobId: job.id, nextRunAt: job.nextRunAt };
  },
});

规则

  • 必须只通过 services、kv、cloud,以及界面中的 host 访问 App。不能导入 App 的代码,也不能在插件之间共享全局单例。
  • 不得导入其他插件的代码。很小的工具函数可以直接复制一份,内置插件的 HTTP 辅助函数就是这样做的。多个内置插件都需要的代码放在 plugins/shared/,例如 wallet 和 card-share 共用的 campus-card.ts。
  • 必须只在响应用户点击时调用 files.pick 和 notify.fix。files.pick 绝不能在助手调用的工具里使用。
  • 应当只对公开的、不需要登录的页面使用普通的 fetch,并声明 requires: ['network']。凡是需要用户会话的请求,都要通过 services.campus.request 发出。

本页目录