Skip to main content
Browse the docs

The developer docs are currently available in Chinese only. The SDK, the CLI and every code sample work the same in any language.

Chapter 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