Manifest v3 · contract_v1 · protocol v2
BJTU Web 插件 API
面向 BJTU MIS Android 的类型化 Web 插件接口。插件通过 SDK 调用版本化
Capability,通过 /api/v3 使用目录、制品和投稿服务;权限、超时、
配额和错误均由同一 Contract Registry 定义。
Compatibility
只运行 contract_v1
Manifest v1/v2 和 P0-A v3(bjtu-service.json)不再安装、
更新或运行,只保留无桥、无网络的数据救援。P0-A 仅能在 publisher
subject、plugin ID 和数据版本均兼容时原位升级;回滚后仍是救援状态。
- 远程 frame 永远没有原生桥。
- optional Capability 首次安装默认关闭。
- 缺少安全 WebView feature 时 required Capability fail closed。
/api/v3服务新目录;/api/v2冻结为旧目录只读。
Quickstart
Vanilla TypeScript + Vite
工具链要求 Node.js 20 至 22。仓库中的 plugin-tooling 工作区提供
@bjtu-mis/plugin-sdk、@bjtu-mis/plugin-cli 与默认模板。
cd plugin-tooling
npm ci
npm run build
node packages/create-bjtu-plugin/dist/index.js my-plugin --id io.example.demo
cd my-plugin
npm install
npm run dev
浏览器开发模式使用 Mock Host。安装 debug APK 后运行
bjtu dev --android,可通过稳定插件 origin 联调 Vite 和
HMR;命令退出时自动撤销 adb reverse 和 debug 开关。
entrypoint、图标、迁移入口和截图均位于 dist/。发布 ZIP 仅包含 bjtu-plugin.json、bjtu-marketplace.json 和 dist/。
bjtu lint --source .
bjtu lint --marketplace .
bjtu test .
bjtu inspect .
bjtu doctor .
bjtu pack .
Contract
精简 Manifest
{
"schema_version": 3,
"id": "io.example.demo",
"name": "Demo",
"version": "1.0.0",
"entrypoint": "index.html",
"icon": "icon.svg",
"capabilities": {
"required": [
"runtime.lifecycle@1",
"configuration.read@1",
"network.request@1"
],
"optional": ["storage.blob@1"]
},
"origins": {
"connect": ["https://api.example.com"],
"media": ["https://cdn.example.com"]
},
"data_schema_version": 1,
"configuration": [{
"key": "API_TOKEN",
"label": "服务令牌",
"description": "用于访问示例服务的令牌。",
"type": "secret",
"required": true
}]
}
空 origins、configuration 和 optional 列表必须
省略。KV/Blob 需要 data_schema_version;版本提升还需要
migration_entrypoint。
| 声明 | 对应规则 |
|---|---|
runtime.lifecycle@1 | 必须位于 capabilities.required。 |
origins.frame | 必须同时声明 remote.frame@1;远程 iframe 始终无桥。 |
origins.navigation | 必须同时声明 navigation.external@1;打开外部链接必须由用户手势触发。 |
configuration | 必须同时声明 configuration.read@1。最多 32 项,key 为全大写 UPPER_SNAKE_CASE;secret 没有默认值。 |
data_schema_version | 仅 KV/Blob 可用;版本大于 1 时必须给出 migration_entrypoint。 |
描述、作者、分类、标签、许可证和截图放入独立
bjtu-marketplace.json。大厅投稿必须提供它;GitHub 直链
安装可以省略。bjtu-plugin.dev.json 只能用于 Mock/HMR,
bjtu pack 会拒绝把它放入发布包。
仓库根 README.md 是可选的投稿说明。客户端会在安装确认前从已固定的 GitHub commit
读取它,最大 1 MiB;请求使用无 Cookie、无认证、禁止重定向的只读客户端。
README 不属于 Manifest、不会打入规范化制品,也不参与包摘要;缺失或读取失败不会阻止安装确认。
| Capability 组 | 用途 |
|---|---|
runtime.lifecycle@1 | 握手、ready、close 与宿主事件。 |
configuration.read@1 | 读取 Manifest 声明的配置。 |
remote.frame@1 / navigation.external@1 | 声明 sandbox 远程 frame,或按用户手势打开已声明的外部链接。 |
identity.profile@1 | 读取当前用户概要。 |
academic.*@1 | 课表、成绩、考试、校历、进度、作业和资源读取。 |
mail.read@1 | 读取邮箱目录和消息。 |
campus.request@1 | MIS/AA/VE 注册表内只读请求。 |
beta Capability 包括 network.request@1、
storage.kv@2、storage.blob@1、
cache.resource@1、
academic.userCourses.command@1、
academic.homework.submit@1 和 mail.send@1。
Android beta Capability 包括 android.accessibility.events@1、
android.accessibility.nodes@1、
android.accessibility.actions@1、
android.packages.read@1 和 android.settings.open@1。
Type-safe API
只通过 SDK 调用
import { BjtuPluginError, createBjtuPluginSdk } from '@bjtu-mis/plugin-sdk';
const bjtu = createBjtuPluginSdk();
const runtime = await bjtu.runtime.handshake();
await bjtu.runtime.ready();
const disposeBack = bjtu.runtime.on('back', () => {
if (!canGoBackInPlugin()) return false;
goBackInPlugin();
return true;
});
const profile = await bjtu.campus.getProfile();
console.log(profile.data, profile.meta.syncedAt);
const controller = new AbortController();
try {
await bjtu.network.request(
{ url: 'https://api.example.com/data' },
{
signal: controller.signal,
onProgress: ({ loaded, total }) => console.log(loaded, total)
}
);
} catch (error) {
if (error instanceof BjtuPluginError) console.error(error.code, error.message);
} finally {
disposeBack();
}
SDK 提供 runtime、configuration、
network、storage.kv、
storage.blob、cache、
navigation、campus、mail 和
android。
校园读取统一返回 { data, meta },meta 含
syncedAt、source、coverage 和
fromCache。
protocol v2 使用 camelCase、独立 capability /
method、取消、订阅事件和统一错误。底层 transport 是私有
实现,不再公开 window.BjtuService。
handshake()返回可用 Capability、协议版本、runtime floor,以及arraybuffer/base64url-chunks-v1二进制传输能力。- 每个调用可传
signal、onProgress和较短的timeoutMs;超出 Capability 上限会被 SDK 拒绝。 - 只有
runtime.on('back', listener)的 listener 返回true时,返回操作才被插件消费。 - 失败统一为
BjtuPluginError,可能的 code 包括permission_denied、capability_unavailable、origin_denied、request_timeout、quota_exceeded、user_cancelled。
User-authorized Android API
受限的系统自动化
21 项严格列举的 Android beta Capability 需要用户在首次安装或更新增量审阅时授权。
授权按 publisher+plugin 持久保存;状态订阅可在进程重启后由后台 runtime 恢复。
无障碍能力还要求用户在系统设置启用 BJTU MIS 服务;未启用时返回
capability_unavailable。桥仍只存在于 publisher+plugin 决定的
稳定本地 main frame,远程 frame 永远无法调用。
const subscription = await bjtu.android.accessibility.events.subscribe({
eventTypes: ['viewClicked', 'windowContentChanged'],
packageNames: ['com.example.app'],
includeSource: true,
persistent: true
});
const removeListener = bjtu.android.accessibility.events.onReceived((event) => {
console.log(event.eventType, event.source);
});
const root = await bjtu.android.accessibility.nodes.getRoot({
maxDepth: 16,
maxNodes: 1024
});
await bjtu.android.accessibility.actions.performNode({
idempotencyKey: crypto.randomUUID(),
nodeId: root.nodeId,
action: 'click'
});
- 每插件最多 16 个事件订阅、每订阅最多 60 events/s;持久订阅可在页面或 App 关闭后恢复,前台页面优先且不重复派发。
- 快照最多 4,096 节点、深度 64;opaque
nodeId30 秒失效。密码和敏感输入不返回文本,并标记sensitive。 - 动作每分钟最多 120 次。首次或增量授权后免逐次弹窗,但仍要求 idempotency key;回执不保存输入、节点文本或手势轨迹。
packages不返回 APK 路径、私有数据或图标;settings.open只允许android.settings.*和可选package:data。- 全局最多 4 个后台 runtime;持续通知提供“全部停止”。删除、禁用、撤销、publisher 变化、声明丢失或复审会立即清理自动化状态。
network、battery和sensors可持久订阅;每插件最多 16 个,网络/电池最多 60 events/s,传感器最多 20 Hz。设备和网络 API 不提供硬件稳定 ID、SSID、BSSID、MAC 或 IP。files、media、share、camera与biometric只能从前台页面唤起系统 UI;后台调用返回foreground_required。文件/媒体只返回隔离 Blob handle,不返回路径或 raw URI。- 定位只允许前台单次读取;录音只能由可见前台 runtime 启动。系统运行时权限、选择器和生物识别弹窗仍由 Android 控制,持久授权不会绕过它们。
当前能力范围只面向 GitHub APK。任何 Google Play 发布必须重新审查 QUERY_ALL_PACKAGES、无障碍和 special-use 前台服务政策。
Campus read API
统一的校园读取接口
身份、教务和邮件的读取响应均为 { data, meta }。
meta 固定包含 syncedAt、source、
coverage 与 fromCache,因此页面可明确呈现数据新鲜度。
| SDK 方法 | 请求参数 | Capability |
|---|---|---|
campus.getProfile(options) | forceRefresh? | identity.profile@1 |
campus.getTimetable(options) | forceRefresh? | academic.timetable@1 |
campus.getScores(request) | term?、courseType?、forceRefresh? | academic.scores@1 |
campus.getHistoryScores(request) | term?、forceRefresh? | academic.scores@1 |
campus.getExams(request) | term?、forceRefresh? | academic.exams@1 |
campus.getCalendar(request) | month?、forceRefresh? | academic.calendar@1 |
campus.getProgress(options) | forceRefresh? | academic.progress@1 |
campus.getHomework(request) | status?、forceRefresh? | academic.homework@1 |
campus.getCourseResources(request) | courseId 必填;term?、folderId?、search?、categoryKey?、forceRefresh? | academic.resources@1 |
mail.listFolders(options) | forceRefresh? | mail.read@1 |
mail.listMessages(request) | folderId?、start?、limit?(1-100)、forceRefresh? | mail.read@1 |
mail.getMessage(messageId, mailbox?) | messageId 必填;mailbox? | mail.read@1 |
只读校园代理
const result = await bjtu.campus.request({
service: 'aa',
method: 'GET',
path: '/registered-path',
query: { term: '2025-2026-2' },
accept: 'application/json'
});
campus.request@1 只允许 mis、aa、ve 中由宿主登记的
GET/HEAD 路径,响应上限为 5 MiB。它不会向插件暴露 Cookie、认证头或明文凭据。
Trust boundary
Origin 与桥由宿主守住
Manifest 只按用途声明 connect、media、
frame 和 navigation。所有来源必须是规范 HTTPS origin,
禁止私网、回环、链路本地、路径和通配符;connect、media
与 frame 还禁止校园域名。
- 桥 origin 固定为 self,不是 Manifest 字段。
- 桥只注入 publisher+plugin 决定的稳定本地 HTTPS main frame。
- 每条消息必须精确匹配 source origin;远程 frame 始终无桥。
- 二进制先握手协商:现代 WebView 使用分块 ArrayBuffer;兼容 WebView 使用 48 KiB 无填充 Base64URL 分片与逐片 ACK。
- 只有缺少
DOCUMENT_START_SCRIPT或WEB_MESSAGE_LISTENER才拒绝运行;缺少 ArrayBuffer feature 不再关闭 Blob/Cache。 - 网络图片保持
network.request → native resource handle → cache.promote,不经过 JavaScript/Base64。
Isolated network
无宿主会话的公网请求
network.request@1 使用无 Cookie、无宿主认证器的独立客户端。
初始请求、DNS 解析和每次重定向都会重新执行 SSRF 检查,并拒绝
Host、Cookie、Content-Length 等
传输层 header。
const response = await bjtu.network.request(
{ url: 'https://api.example.com/events', method: 'GET' },
{ onProgress: ({ loaded, total, phase }) => updateProgress(loaded, total, phase) }
);
if (response.bodyType === 'resource' && response.resource) {
await bjtu.cache.promote(response.resource.handle, 'events/latest');
}
- 六种 HTTP 方法;JSON、文本、FormData 和 Blob handle 请求体。
- 默认 15 秒、上限 60 秒;最多 5 次重定向。
- 每插件并发 4、每 origin 并发 2。
- JSON/文本内联上限 1 MiB;更大或二进制响应返回资源 handle。
Transactional data
KV2、Blob 与 Cache
- KV:10 MiB、单项 256 KiB、1024 keys,支持 CAS、batch、transaction 和 watch。
- Blob:不可变内容寻址,每插件 256 MiB、单项 64 MiB。
- Cache:LRU,每插件 512 MiB、全局 1 GiB、单项 250 MiB。
- 数据按 publisher+plugin 隔离,以 AES-GCM 分块加密并原子维护索引。
- 资源通过
/__bjtu/resources/<handle>提供 GET、HEAD 和 Range。 - 升级使用影子迁移和上一版本快照,失败时原子回滚。
storage.kv 提供 get、set、remove、
keys、usage、batch、transaction、
export、import 和 watch。
storage.blob 提供 put、getInfo、delete;
cache 提供 put、match、promote、
deleteHandle、delete、pin 和 usage。
调用 storage.blob.put 或 cache.put 前必须完成 runtime.handshake()。SDK 会在宿主协商的 ArrayBuffer 或 Base64URL 分片通道上传数据。
import { createBjtuPluginMigrationSdk } from '@bjtu-mis/plugin-sdk';
const migration = createBjtuPluginMigrationSdk();
const previous = await migration.storage.get('settings');
await migration.storage.set('settings', upgradeSettings(previous));
await migration.commit();
migration_entrypoint 只能使用影子 KV 的 get、set、
remove、keys、usage、clear 和显式
commit()。网络、校园读取和 Command Capability 在迁移中不可用。
Command capabilities
逐次确认与幂等
除首次或增量 Capability 审阅后持久授权的 Android 命令(无障碍动作、文件保存、
通知、日历写入和录音)外,所有改变校园或宿主状态的能力都要求用户逐次确认和
idempotency key。这些 Android 命令免逐次弹窗,但仍要求 idempotency key、
摘要回执和运行时配额。
同 key 与同请求摘要返回原回执;同 key 与不同摘要返回
idempotency_conflict。加密回执不保存请求正文,保留 7 天,
每插件最多 1024 条。邮件和作业不会静默发送或提交。
| SDK 方法 | 必填字段 | 超时 |
|---|---|---|
campus.saveUserCourse(key, course) | idempotencyKey、course | 15 秒 |
campus.deleteUserCourse(key, id) | idempotencyKey、id | 15 秒 |
campus.submitHomework(request) | idempotencyKey、homeworkId、courseId | 60 秒 |
mail.send(request) | idempotencyKey、to、subject | 60 秒 |
Plugin directory REST API
目录、制品与投稿均使用 /api/v3
/api/v3 只返回 contract_v1 插件。目录、详情和更新解析
使用 camelCase;投稿状态接口保留 source_url、plugin_id、
commit_sha 等记录字段名。失败响应统一为 { error: { code, message } }。
| 接口 | 认证 | 用途 |
|---|---|---|
GET /api/v3/plugins | 否 | 目录列表。支持 query?、category?、cursor?、limit?;limit 为 1-50,默认 20。 |
GET /api/v3/plugins/:id | 否 | 详情,包含 Capability、origin、digest、发布者 identity、数据版本和制品 URL。 |
POST /api/v3/plugins/resolve-updates | 否 | 最多提交 100 个已安装记录,解析更新、publisher 不匹配和 P0-A 替换状态。 |
GET /api/v3/plugins/:id/versions/:commit/artifact | 否 | 下载规范化 ZIP,响应含 ETag、Digest: sha-256=... 和 X-Content-Type-Options: nosniff。 |
GET /api/v3/plugins/:id/versions/:commit/icon | 否 | 读取图标,使用 sandbox CSP。 |
POST /api/v3/submissions | GitHub 会话 + CSRF | 提交公开 GitHub 仓库根链接,创建异步校验任务。 |
GET /api/v3/submissions/:id | 本人或管理员 | 读取任务状态、识别到的 plugin ID、commit 和错误信息。 |
GET /api/v3/me/plugins | GitHub 会话 | 读取当前用户的 contract_v1 投稿和插件记录。 |
POST /api/v3/plugins/:id/revalidate | 插件所有者 + CSRF | 为同一公开仓库重新创建校验任务。 |
POST /api/v3/plugins/:id/unpublish | 插件所有者 + CSRF | 下架当前插件。 |
POST /api/v3/plugins/:id/reports | GitHub 会话 + CSRF | 提交举报;reason 必填,details 可选。 |
目录与更新
GET /api/v3/plugins?category=academic&limit=20
{
"apiVersion": 3,
"contractProfile": "contract_v1",
"items": [{
"id": "io.example.demo",
"version": "1.0.0",
"runtimeFloor": 2,
"capabilities": { "required": ["runtime.lifecycle@1"], "optional": [] },
"origins": { "connect": [], "media": [], "frame": [], "navigation": [] },
"archiveSha256": "...",
"packageDigestSha256": "..."
}],
"nextCursor": null
}
POST /api/v3/plugins/resolve-updates
Content-Type: application/json
{
"installed": [{
"id": "io.example.demo",
"commitSha": "previous-commit",
"publisherSubjectId": "github-owner:12345",
"contractProfile": "contract_v1"
}]
}
投稿和 CSRF
先经 GET /api/v1/auth/github/start 完成 GitHub 登录;再使用
GET /api/v1/auth/me 取得 csrfToken,在所有写请求中发送
X-CSRF-Token。投稿只接受 https://github.com/{owner}/{repo}
形式的公开仓库根链接;每个账号每天最多提交或重校验 10 次。
POST /api/v3/submissions
X-CSRF-Token: <csrfToken>
Content-Type: application/json
{ "repositoryUrl": "https://github.com/example/course-reminder" }
// 202 Accepted
{
"apiVersion": 3,
"id": "submission-id",
"status": "queued",
"requiredSchemaVersion": 3,
"requiredManifest": "bjtu-plugin.json",
"requiredMarketplace": "bjtu-marketplace.json",
"contractProfile": "contract_v1"
}
校验 worker 只下载归档、静态扫描并规范化打包 Manifest、marketplace 与 dist/。首次发布后 publisher subject 固定为 GitHub owner 数值 ID;owner 转移需管理员审批。
Release
发布检查清单
- 只保留最小 required/optional Capability 和最小 origin。
- 所有可执行 JavaScript 本地打包;remote iframe 使用受限 sandbox。
- 数据 schema 变化提供仅可访问影子存储的 migration。
- 运行 lint、Mock、确定性 pack、平台与 Android 测试。
- 包内不含 dev 配置、凭据、Cookie、token、个人信息或构建缓存。
统一错误包括 permission_denied、
capability_unavailable、invalid_request、
foreground_required、
origin_denied、network_timeout、
http_error、quota_exceeded、
resource_too_large、migration_failed、
user_cancelled 和 idempotency_conflict。
Session keep-alive
插件 MIS 会话保活
android.session.keepAlive@1(beta,runtime 3)支持前台申请/续租、后台查询/释放限时租约,首次或增量授权后生效。每插件最多 2 个租约,创建起最长 60 分钟;持续通知可停止,撤销/更新/删除会清理。不会暴露凭据,也不保证进程永久存活。