插件
Obsidian Arc 的核心是一个通用的 AI 对话站点。只有部分实例才需要的功能——比如某个社群站点要加在账户上的字段,或者某个运营方自建的风控服务——以插件的形式存在:放在 plugins/ 目录下,按需编译进二进制。
没编译进去的插件不贡献任何代码、路由、数据表或界面文字;二进制仍然是部署说明里承诺的那一个文件。
这一页讲编译进二进制的插件。更常用的是插件包:拖进后台就装好,删除即真删。
编译进来的插件在后台的「插件」页里管理,用法和浏览器的扩展页一样:安装、启用、禁用、卸载,每个插件都有自己的清单。
本仓库不带插件
这个仓库只有插件机制,没有任何具体的插件:plugins/ 是空的,默认构建就是核心本身。具体站点的插件放在那个站点自己的仓库里,构建时叠到这棵源码树上——Go 只允许 internal/ 目录里的代码引用 internal/ 下的包,所以插件必须真的落在这棵树里,而不是从外面引用它。
一个插件在树里占三处:
| 位置 | 内容 |
|---|---|
plugins/<名字>/ | 服务端:plugin.Register 的实现、迁移和清理脚本、测试 |
web/src/plugins/<名字>/<名字>.plugin.ts | 浏览器端:声明式的字段、设置卡片、列表与文案 |
cmd/server/plugin_<名字>.go | 一行带构建标签的空白引用,把服务端编进二进制 |
设置键、数据表和迁移版本号一旦发布就不再改名:一个已经在用某个插件的实例,只要继续把它编译进去,什么都不用迁移。
选择编译哪些插件
make build # 默认:只有核心
make build PLUGINS="alpha beta" # 带上 plugins/alpha 和 plugins/beta
go build ./cmd/server # 不带任何构建标签 = 只要核心每个插件对应一个构建标签 plugin_<名字>。Docker 镜像同样接受 --build-arg PLUGINS="…",默认值和 Makefile 一致。
make version 会顺带打印这次构建带了哪些插件,make release 也会把它写进 dist/PLUGIN_LIST。make deploy 在服务器上发现上一次部署带着的插件这一次没带时会拒绝继续,因为这样会让一个正在用它的实例悄悄失去功能;确实要去掉时,先在后台卸载它,再加 DROP_PLUGINS=1。服务器上的记录叫 PLUGIN_LIST,放在它 compose 目录下。新列表只在 docker compose up 成功后写入;若服务器上还没有这个文件,部署开始时先把旧记录复制过去,内容不变。
在后台管理插件
「插件」页列出本程序带有的每个插件和它在本实例的状态:
| 状态 | 含义 |
|---|---|
| 未安装 | 编译进来了,但没有为它执行迁移、它的设置不被识别、它的钩子都不运行 |
| 已启用 | 正常工作 |
| 已禁用 | 数据表和设置都在,功能全部停止:路由返回 404,设置不列出也不能写入,账户字段不再读写,登录守卫不再检查,浏览器也不再下载它的前端代码 |
| 本构建未包含 | 由带着它的构建安装过,当前这个二进制没有它;在重新带上之前不起作用 |
- 安装:可以选择是否安装后立即启用,并在同一步里填好它的初始设置(插件前端声明了哪些设置项,安装面板里就出现哪些,其中只画出当前管理员可写的,见下文「权限」)。迁移、初始设置和安装记录在同一个事务里写入,任何一步失败都会整体回滚。
- 禁用:保留一切,停止一切,需要两步验证码。
- 卸载:需要两步验证码。默认保留数据,以后重新安装会接着用;勾选「同时删除数据」会删除它的设置,并执行插件自带的清理脚本删除它的数据表和字段,同时清除它的迁移记录,重新安装时从空表开始。
- 清单:版本、作者、许可证、说明,以及从各个扩展点读出来的实际接入内容——设置项、账户字段、注册与登录检查、接口、控制台命令、数据迁移、后台页面。
- 每次安装、启用、禁用、卸载都会记进安全日志(事件
plugin)。 - 控制台里是
plugin list、plugin show、plugin install、plugin enable、plugin disable --code、plugin uninstall --code [--purge] --yes。
权限
「插件」页拆成三项后台权限,超级管理员在管理员页按需授予:
| 权限 | 能做什么 |
|---|---|
plugins | 查看插件列表与清单 |
plugins_manage | 安装、启用、禁用(也能查看) |
plugins_remove | 卸载,保留数据(也能查看) |
plugins_remove 只能卸载,并保留数据,以后重装接着用。同时删除数据(后台的勾选项,控制台的 --purge)只限超级管理员:清理脚本以数据库属主的身份删表删列,这一步不在委派的卸载权限之内。持有 plugins_remove 的管理员勾选它时,请求返回 403(admin_permission_denied),什么都不改。
安装时随请求提交的每个设置项,还要各自通过该键自己的权限检查,与共享设置接口对这个键的要求相同:插件为它声明的所属后台权限,没有声明的按核心的归属。plugins_manage 不等于能设置插件的所有键。提交的键里只要有一个不够权限,整次安装就被拒绝(403,admin_permission_denied),并且在检查两步验证码和执行迁移之前就返回,什么都不写。后台的安装面板只提交服务端按本次查看者标明可写的键(插件列表与详情返回的 writable_settings),所以只持有 plugins_manage、又没有被授予某个受限键的管理员,不会因为一个自己没动过的键而安装失败;未填写的密钥照旧不提交。
禁用和卸载无论持有哪项权限,都还要求操作者本人已开启两步验证并输入验证码:插件可能正站在注册入口前面,被盗的会话悄悄关掉它就等于把门打开了。
升级时
第一次用这个版本启动时,还没有任何插件状态记录。服务器会按「痕迹」决定:插件的迁移已经执行过,或者它的设置已经存过值,就视为这个实例一直在用它,直接记为已启用;否则记为未安装,等管理员去装。所以之前就在用某个功能、后来才把它做成插件的实例,升级后什么都不用做。
卸载后留下的记录不会被当成「没有记录」:保留数据卸载的插件,下次启动不会因为数据还在就被自动装回去。
多实例
插件状态在做出改动的那个进程里立即生效;指向同一个数据库的其他进程要等重启才会读到新状态。在那之前,如果卸载时删除了数据,它们查询被删掉的字段会报错。多实例部署时,改完插件请重启其余实例。
去掉一个插件会怎样
说的是从构建里去掉(PLUGINS 里不再列出它),不是在后台卸载:
- 它的路由不再存在(返回 404),它的设置键会被后台当作未知键拒绝写入;
- 数据库里它留下的数据原样保留:列还在、表还在,核心读写时绕开它们;
- 它留下的设置行不会出现在后台设置接口里——那可能是它的密钥,而负责给密钥打码的正是这个已经不在的插件;
- 插件页把它列为「本构建未包含」;以后重新编译进来,它接着用原来的数据和原来的状态。
插件能改动什么
插件只能通过核心模块显式开放的扩展点接入,方向永远是插件依赖核心、核心不知道任何插件:
| 扩展点 | 用途 |
|---|---|
database.Migrate 的插件迁移目录 | 插件自己的表和列。从核心搬出来的迁移保留原版本号;插件新写的迁移命名为 <插件>_NNNN_*.sql |
settings.Define / settings.AddCaptchaMode | 插件的设置项(默认值、是否为密钥、所属后台权限、校验)和注册验证码模式 |
user.DefineField + auth.Service.SetFieldRule | 账户上的附加字段:自动参与查询、扫描、写入、唯一性检查与后台搜索;注册时是否必填由插件的设置决定 |
auth.Service.AddGuard | 注册 / 登录前的检查,令牌放在请求体的 guards 里。登录守卫的拒绝同样适用于 SSH 控制台的密码入口,以及第三方登录的回调和补全表单;这几处没有请求体,传给守卫的令牌为空,要求令牌的守卫应在这里拒绝 |
auth.Handlers.Extend | /api/site 里 plugins.<名字> 下给浏览器的配置 |
oauth.Service.BindSubject | 某个登录提供方的主体号即某个账户字段的值 |
admin.Handlers.Mount / console.Register | 后台接口(和其他后台接口走同一层权限包装)与控制台命令 |
invite.Handlers.DecorateInvitees | 给邀请人自己的邀请列表补充信息 |
plugin.Host.Handle | 后台以外的公开接口(比如机器人 Webhook) |
plugin.Host.AllowOrigin | 页面需要加载第三方服务脚本时放宽 CSP 的来源 |
plugin.Purger | 卸载并删除数据时执行的清理脚本 |
每个扩展点登记的东西都带着所属插件的名字,核心在运行它之前先问一次这个插件现在是否启用——插件自己的代码不需要检查任何状态,关掉它也不需要重启。
浏览器端
每个插件在 web/src/plugins/<名字>/<名字>.plugin.ts 有一个对应模块,构建成一个独立的小 chunk(几 KB),只有当 /api/site 列出了这个插件时才会下载。
插件模块只做声明,不写界面:账户字段长什么样、守卫怎么拿令牌、设置卡片里有哪些控件、一张分页列表、账户面板里的几个按钮、错误码和通知的文案、它自己的中英文字典。核心用自己的组件把这些画出来,所以插件的设置会和同页其他设置一起标记未保存、一起提交、一起报错,也不会带来一套风格不一致的控件。
插件还可以声明自己的后台子页面(adminPages):一个路径、标题、图标和打开它需要的权限。设置卡片和列表的 page 写成 plugin:<路径> 就会放到这个页面上,由核心的通用页面加载、保存。插件的设置和列表可以放在它自己的页面里;插件被禁用或卸载后,这个页面随之从后台导航里消失。
插件还可以往 API 密钥页放东西(keyIssuing):顶部的一条提示,和一个每创建一个密钥都要回答的确认——点「创建」时弹出警告,要求输入指定的字才能继续;取消或输错,密钥就不会创建。要输入的字由样式表从属性里画出来,不写在页面的文字里,所以选不中、复制不了,输入框也不接受粘贴和拖入:它要的是让人读一遍再亲手打出来。这是一句提醒,不是防护,也不是服务器端的检查——不走这个页面、直接调接口创建的密钥不会被问到。没有后端的插件包(只有 manifest.json 和 web/ui.js)就够做这件事,用 arcpack pack 打包。