Skip to main content
浏览文档目录
第 5 章

上下文 API(ctx)

ctx 上每个方法的签名、行为与上限

run(args, ctx) / describe(args, ctx) / undo(result, ctx) 拿到的 ctx。除 t、now、newId 外全部是异步的 (跨沙箱 RPC,返回 Promise)。能力字段只在声明了对应权限时存在;read 模式下写方法存在但调用即抛错(见权限)。

能力随调用结束作废:把 ctx 存起来、在定时器里再用,只会得到「This plugin action has already finished」。

基础

TypeScript
ctx.plugin        // { id, version }
ctx.mode          // 'read' | 'navigate' | 'write'
ctx.locale        // 'en' | 'zh' …(当前界面语言)
ctx.t(key, opts)  // 本插件文案(同步)
ctx.now()         // Date
ctx.newId(prefix) // 'prefix-1727…-a1b2c':本地生成,足够唯一

courses(context.read:courses)

TypeScript
ctx.courses.list(): Promise<Array<{ id, name, code | null }>>
ctx.courses.get(id): Promise<{ id, name, code } | null>

assignments(context.read:assignments)

TypeScript
ctx.assignments.list(q?: { courseId? }): Promise<PluginAssignment[]>
ctx.assignments.get(id): Promise<PluginAssignment | null>
// PluginAssignment: { id, courseId, course, title, dueDate: ISO | null, status: 'todo'|'in-progress'|'done', isQuiz,
//                     points, grade }   // points / grade 只有 context.read:grades 才有值,否则 null

materials(context.read:files)

TypeScript
ctx.materials.search({ query, courseId?, types?, limit? }): Promise<{ results: MaterialHit[], mode: 'vector'|'keyword', note? }>
// MaterialHit: { ref, courseId, course, type, title, excerpt(≤1200 字), url?, locator? }

与 UniMind 的课程资料检索同一个端口;套餐不支持时抛出可读原因。

calendar(calendar.read / calendar.write)

TypeScript
ctx.calendar.list({ from, to }): Promise<PluginCalendarEvent[]>
//   [from, to) 之间(跨度 ≤ 400 天)的用户日程与本插件的事件;循环日程已展开成具体的某一次;按开始时间排序,≤ 1000 条
ctx.calendar.owned(): Promise<PluginCalendarEvent[]>       // 本插件建的全部事件(循环系列不展开)
ctx.calendar.create(events: PluginEventInput[]): Promise<PluginCalendarEvent[]>   // ≤ 200 条/次
ctx.calendar.update(id, patch): Promise<void>
ctx.calendar.remove(ids): Promise<void>
ctx.calendar.ready?(): Promise<'ready' | 'loading' | 'unsynced'>
  • 时间一律 ISO 8601 带时区;宿主统一存成 UTC。
  • 新建的事件自动盖章 ownerPlugin = 你的 id;只能改 / 删自己建的事件,碰别的抛错;不存在的 id 在 remove 里忽略。
  • 指定 id 新建:已存在且属于你 = 更新(重复导入不重复建)。id 规则 ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$。
  • rrule 用 RFC 5545(不带 RRULE: 前缀),不认识的规则被拒。
  • ready():日历是不是当前账号的完整状态。「我的事件一条都不在」要先问它:ready = 真被用户删了;loading = 还在加载,稍后再试; unsynced = 这次启动没和云端合并上(先联网)。宿主不提供时没有这个方法,按「不知道」处理。

storage(storage)/ deviceStorage(storage.device)

TypeScript
get<T>(collection, id): Promise<{ id, data: T, updatedAt } | null>
list<T>(collection): Promise<Array<{ id, data: T, updatedAt }>>
put(collection, id, data): Promise<void>
remove(collection, id): Promise<void>

规则与配额见存储。

net(network:user-granted,仅 write 模式)

TypeScript
ctx.net.fetchText(url): Promise<{ text, finalUrl, contentType | null }>
  • 只收公网 https(webcal:// 自动换成 https://);起点与每一跳重定向都做 SSRF 检查;≤ 5 MB;20 秒超时;不带任何 cookie。
  • 写了 networkHosts 时,起点与重定向终点的主机都必须在白名单里。
  • 失败抛出错误码,自己翻译成人话:invalid-url、url-too-long、unsupported-scheme、credentials-in-url、blocked-host(内网 / 保留地址)、blocked-redirect、too-many-redirects、too-large、timeout、dns-failed、http-<状态码>、network-error: …。

files(files.open / files.save,仅 write 模式)

TypeScript
ctx.files.openText({ extensions: ['csv','txt'], maxBytes? }): Promise<{ name, text } | null>   // 用户取消 → null;默认 ≤ 2MB,最多 10MB
ctx.files.saveText({ suggestedName, extensions, content }): Promise<string | null>            // 返回保存的文件名(不含目录)

同一时间只能开一个文件对话框(另一个在开着时抛 dialog-busy)。

ui(ui.panel,navigate / write 模式)

TypeScript
ctx.ui.openPanel(params?: Record<string, string>): Promise<void>   // 以标签页打开本插件的面板;params 交给面板的 props.params