宿主服务
内置插件需要做自身以外的事情时,都要通过 `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 | 系统的文件选择器,以及对话中附件的文字(列出、读取、搜索) |
settings | App 和插件的全部设置:列出、修改、打开对应页面 |
phone | Copilot 的手机会话(供内置的 Copilot 插件使用) |
browser | 在系统浏览器中打开网址 |
crypto | sha256(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发出。