写一个插件
六个扩展点、一份 manifest、一个独立进程。以及为什么插件不能自己画界面。
从模板开始
官方插件模板里 manifest、 RPC 骨架、schema 驱动的配置表单、日志与投递统计都已经接好,克隆下来改就行。
已有的官方插件也都是公开仓库,可以直接当范例读——插件页列了全部。
插件是一个独立进程
不是在安时进程里跑的脚本,是一个被拉起来的子进程,通过 stdio 收发 JSON-RPC。
这样换来两件事:插件崩了不影响本地写库,以及插件的权限可以逐项声明。 代价是插件不能直接操作数据库,也不能直接画界面——见下面两节。
六个扩展点
| 扩展点 | 干什么 | 默认状态 |
|---|---|---|
sync |
把外部系统的任务或日程搬进来。task 双向(完成状态回写),event 只拉 | 装上后需配置账号 |
replica |
多端同步的传输后端。只搬加密好的字节,看不懂内容,也不做合并判断 | 同上 |
notificationChannel |
提醒到点时除了系统通知,还发到别处 | 同上 |
dayMarks |
给某一天贴个短标签(节假日、纪念日)。这是公开知识,谁算都一样 | 装上即生效 |
calendarOverlay |
往月视图格子贴角标、往侧栏加统计。这是你的个人数据(打卡、请假) | 默认关闭 |
widget |
往浮窗那一列卡片里加一张。也是个人数据 | 默认关闭 |
一个插件至少要贡献一个扩展点,什么都不贡献的插件永远不会被调用,安装时就会被拒。
sync 和 replica 不能同时声明——前者要理解外部系统的模型,后者只搬字节,
混在一起用户会分不清「它到底在同步什么」。
声明式权限
yinian-plugin.json 里的 permissions 有四项,安装前会摊开给用户看:
net:要访问的域名白名单。写"*"会被界面高亮告警。spawn:要执行的外部命令名。fs:要读写的路径(相对插件自己的dataDir)。访问dataDir本身不用声明。api:回环 HTTP API 的只读 scope。
api 这一项是真实生效的:注入给插件的 token 只带声明过的 scope,
没声明的路由一律 403。一期没有任何写 scope——插件想改数据只有一条路,
从 sync.pull 把数据交回宿主,由宿主落库。
这个 token 和 Agent 的那个不是同一个东西:它进程内有效、不落盘,
宿主重启会重新签发,插件停用或卸载立刻撤销。每次从 plugin.init 的参数里取,
不要存进 dataDir 或写进日志。
别图省事把
fs写成"*"。"*"是给「目录由用户填」的插件准备的 (比如把加密字节写进 iCloud Drive 同步目录那种),写死路径写不了。 能落在dataDir里的插件写"*"等于向用户多要了整个文件系统。 反过来,真要读写用户指定目录的插件必须声明它——不声明的话安装界面会显示 「未申请任何额外权限」,而那句话是假的。
你不能自己画界面
插件的展示面一律是「声明式数据 + 宿主渲染」。 你给的是
(标签, 值, 语义档位),颜色、字体、排版、条数上限全归宿主。
没有 HTML,没有 iframe,没有自定义布局,三个展示类扩展点都没有例外。 这不是还没做,是刻意不做的:
- 留了这个口子,所有插件都会走那条路,封闭布局就白定义了——插件会长得五花八门, 深浅主题下各种不搭。
- 更要紧的是 iframe 里的
fetch会绕过permissions.net声明, 插件的联网面从此不可审计。
配置表单同理,用 JSON Schema 声明,宿主渲染。
个人数据默认关闭
calendarOverlay 和 widget 这两个扩展点默认关闭,要用户逐个显式启用;
关着的时候宿主根本不发 RPC。
「装了插件」不等于「同意把我的会议标题画在屏幕最上层」。「拉一次数据」本身就是 需要授权的行为,拿到数据再决定显不显示已经晚了。
所以 widget 的 description 是必填的(calendarOverlay 那边可选):
它画在一个常驻置顶窗口上,「飞书日历」四个字说不清会显示什么、数据从哪来,
而这是用户决定要不要启用的唯一依据。
打包与安装
发布产物是单个 zip,解压后顶层就是包结构,不要多包一层目录。
version 要是合法 semver,发布时 Git tag 必须与它完全一致。
装完落在 ~/.nikou-agenda/plugins/<plugin-id>/<version>/。
minHostVersion 是必填的。如果它高于用户当前的安时版本,宿主会读 versions.json
去找一个能装的旧版本——和 Obsidian 一个机制。
不认识的取值会被拒,不会静默忽略
枚举字段(resources、actions、fields、modes、hooks、permissions.api 等)
写了不认识的值直接拒绝安装。这是刻意的:静默忽略会让你以为声明生效了,
然后在一个不该出现的 403 上排查半天。
完整的 manifest 字段表、RPC 协议与错误码全表在模板仓库的 README 里, 模板本身跑起来就是一个能装进安时的可用插件。