返回主页

Manifest v3 · contract_v1 · protocol v2

BJTU Web 插件 API

面向 BJTU MIS Android 的类型化 Web 插件接口。插件通过 SDK 调用版本化 Capability,通过 /api/v3 使用目录、制品和投稿服务;权限、超时、 配额和错误均由同一 Contract Registry 定义。

Manifestv3
Profilecontract_v1
Protocolv2
Runtime floor2
Bridgehost-fixed self
Directory API/api/v3

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.jsonbjtu-marketplace.jsondist/

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
  }]
}

originsconfiguration 和 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@1MIS/AA/VE 注册表内只读请求。

beta Capability 包括 network.request@1storage.kv@2storage.blob@1cache.resource@1academic.userCourses.command@1academic.homework.submit@1mail.send@1。 Android beta Capability 包括 android.accessibility.events@1android.accessibility.nodes@1android.accessibility.actions@1android.packages.read@1android.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 提供 runtimeconfigurationnetworkstorage.kvstorage.blobcachenavigationcampusmailandroid。 校园读取统一返回 { data, meta },meta 含 syncedAtsourcecoveragefromCache

protocol v2 使用 camelCase、独立 capability / method、取消、订阅事件和统一错误。底层 transport 是私有 实现,不再公开 window.BjtuService

  • handshake() 返回可用 Capability、协议版本、runtime floor,以及 arraybuffer / base64url-chunks-v1 二进制传输能力。
  • 每个调用可传 signalonProgress 和较短的 timeoutMs;超出 Capability 上限会被 SDK 拒绝。
  • 只有 runtime.on('back', listener) 的 listener 返回 true 时,返回操作才被插件消费。
  • 失败统一为 BjtuPluginError,可能的 code 包括 permission_deniedcapability_unavailableorigin_deniedrequest_timeoutquota_exceededuser_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 nodeId 30 秒失效。密码和敏感输入不返回文本,并标记 sensitive
  • 动作每分钟最多 120 次。首次或增量授权后免逐次弹窗,但仍要求 idempotency key;回执不保存输入、节点文本或手势轨迹。
  • packages 不返回 APK 路径、私有数据或图标;settings.open 只允许 android.settings.* 和可选 package: data。
  • 全局最多 4 个后台 runtime;持续通知提供“全部停止”。删除、禁用、撤销、publisher 变化、声明丢失或复审会立即清理自动化状态。
  • networkbatterysensors 可持久订阅;每插件最多 16 个,网络/电池最多 60 events/s,传感器最多 20 Hz。设备和网络 API 不提供硬件稳定 ID、SSID、BSSID、MAC 或 IP。
  • filesmediasharecamerabiometric 只能从前台页面唤起系统 UI;后台调用返回 foreground_required。文件/媒体只返回隔离 Blob handle,不返回路径或 raw URI。
  • 定位只允许前台单次读取;录音只能由可见前台 runtime 启动。系统运行时权限、选择器和生物识别弹窗仍由 Android 控制,持久授权不会绕过它们。
分发政策

当前能力范围只面向 GitHub APK。任何 Google Play 发布必须重新审查 QUERY_ALL_PACKAGES、无障碍和 special-use 前台服务政策。

Campus read API

统一的校园读取接口

身份、教务和邮件的读取响应均为 { data, meta }meta 固定包含 syncedAtsourcecoveragefromCache,因此页面可明确呈现数据新鲜度。

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 只允许 misaave 中由宿主登记的 GET/HEAD 路径,响应上限为 5 MiB。它不会向插件暴露 Cookie、认证头或明文凭据。

Trust boundary

Origin 与桥由宿主守住

Manifest 只按用途声明 connectmediaframenavigation。所有来源必须是规范 HTTPS origin, 禁止私网、回环、链路本地、路径和通配符;connectmediaframe 还禁止校园域名。

  • 桥 origin 固定为 self,不是 Manifest 字段。
  • 桥只注入 publisher+plugin 决定的稳定本地 HTTPS main frame。
  • 每条消息必须精确匹配 source origin;远程 frame 始终无桥。
  • 二进制先握手协商:现代 WebView 使用分块 ArrayBuffer;兼容 WebView 使用 48 KiB 无填充 Base64URL 分片与逐片 ACK。
  • 只有缺少 DOCUMENT_START_SCRIPTWEB_MESSAGE_LISTENER 才拒绝运行;缺少 ArrayBuffer feature 不再关闭 Blob/Cache。
  • 网络图片保持 network.request → native resource handle → cache.promote,不经过 JavaScript/Base64。

Isolated network

无宿主会话的公网请求

network.request@1 使用无 Cookie、无宿主认证器的独立客户端。 初始请求、DNS 解析和每次重定向都会重新执行 SSRF 检查,并拒绝 HostCookieContent-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 提供 getsetremovekeysusagebatchtransactionexportimportwatchstorage.blob 提供 putgetInfodeletecache 提供 putmatchpromotedeleteHandledeletepinusage

二进制写入先握手

调用 storage.blob.putcache.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 的 getsetremovekeysusageclear 和显式 commit()。网络、校园读取和 Command Capability 在迁移中不可用。

Command capabilities

逐次确认与幂等

除首次或增量 Capability 审阅后持久授权的 Android 命令(无障碍动作、文件保存、 通知、日历写入和录音)外,所有改变校园或宿主状态的能力都要求用户逐次确认和 idempotency key。这些 Android 命令免逐次弹窗,但仍要求 idempotency key、 摘要回执和运行时配额。 同 key 与同请求摘要返回原回执;同 key 与不同摘要返回 idempotency_conflict。加密回执不保存请求正文,保留 7 天, 每插件最多 1024 条。邮件和作业不会静默发送或提交。

SDK 方法必填字段超时
campus.saveUserCourse(key, course)idempotencyKeycourse15 秒
campus.deleteUserCourse(key, id)idempotencyKeyid15 秒
campus.submitHomework(request)idempotencyKeyhomeworkIdcourseId60 秒
mail.send(request)idempotencyKeytosubject60 秒

Plugin directory REST API

目录、制品与投稿均使用 /api/v3

/api/v3 只返回 contract_v1 插件。目录、详情和更新解析 使用 camelCase;投稿状态接口保留 source_urlplugin_idcommit_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,响应含 ETagDigest: sha-256=...X-Content-Type-Options: nosniff
GET /api/v3/plugins/:id/versions/:commit/icon读取图标,使用 sandbox CSP。
POST /api/v3/submissionsGitHub 会话 + CSRF提交公开 GitHub 仓库根链接,创建异步校验任务。
GET /api/v3/submissions/:id本人或管理员读取任务状态、识别到的 plugin ID、commit 和错误信息。
GET /api/v3/me/pluginsGitHub 会话读取当前用户的 contract_v1 投稿和插件记录。
POST /api/v3/plugins/:id/revalidate插件所有者 + CSRF为同一公开仓库重新创建校验任务。
POST /api/v3/plugins/:id/unpublish插件所有者 + CSRF下架当前插件。
POST /api/v3/plugins/:id/reportsGitHub 会话 + 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_deniedcapability_unavailableinvalid_requestforeground_requiredorigin_deniednetwork_timeouthttp_errorquota_exceededresource_too_largemigration_faileduser_cancelledidempotency_conflict

Session keep-alive

插件 MIS 会话保活

android.session.keepAlive@1(beta,runtime 3)支持前台申请/续租、后台查询/释放限时租约,首次或增量授权后生效。每插件最多 2 个租约,创建起最长 60 分钟;持续通知可停止,撤销/更新/删除会清理。不会暴露凭据,也不保证进程永久存活。