插件系统
插件系统是Molore的能力扩展机制:插件运行在本机后端进程内,可以提供 REST 接口、MCP 工具和定时同步任务。它解决的问题是——在不改动主干代码的前提下,把外部数据源(Notion、Pocket、Readwise)和外部 AI 客户端(经 MCP)接入本地知识库。本地优先架构下插件与后端同进程、同权限,因此系统选择了「声明制」而非「沙箱制」:权限如实声明、可读可查,但不做运行时拦截。
概念
| 概念 | 说明 |
|---|---|
| manifest | 插件的元数据描述文件(manifest.json),声明 id、入口、默认启停、配置 schema 与权限清单 |
| BasePlugin | 插件基类(app/plugins/base.py),定义生命周期钩子与可选能力(路由 / MCP 工具 / 同步入口) |
| 权限声明 | manifest 的 permissions 字段,枚举口径见 KNOWN_PERMISSIONS(app/plugins/base.py:12) |
| 用户级启停 | 启用/禁用状态按用户存储在 user.settings.plugins,路由与 MCP 工具全局注册、按用户状态判定 |
| 自动同步 | 同步类插件的定时任务,由 app/services/plugin_scheduler.py 按用户配置的间隔调度 |
功能清单
- manifest 驱动加载:
PluginManifest(pydantic)校验 manifest,entrypoint指向模块.类路径,插件管理器PluginManager(app/plugins/manager.py)从内置目录与用户插件目录发现并加载。 - BasePlugin 生命周期:子类构造签名固定为
(manifest, config),可选实现生命周期与能力钩子(见下表)。 - 用户级启停与配置:
set_enabled/set_config读写user.settingsJSON,进程内 per-user 锁防并发写竞态;凭据类配置落库前加密存储,读取接口不回显明文。 - 权限声明展示:安装、启用、列表接口都带出 permissions 清单,前端在启用前展示给用户决策。
- 插件 REST 接口:
get_routers()返回的路由挂载在/api/v1/plugins/{plugin_id}下。 - MCP 工具暴露:
register_mcp_tools(mcp)在共享 FastMCP 实例上注册工具,随启用自动加入 MCP Server、禁用即移除;外部客户端经 SSE 地址/api/v1/mcp/sse接入,需携带Authorization: Bearer <access_token>。 - 定时同步:支持后台同步的插件实现
run_sync(user, db),调度器按IntervalTrigger触发,间隔可选 30 分钟 / 1 小时 / 6 小时 / 24 小时,面板显示上次/下次同步时间与上次错误。 - 从 URL 安装(仅桌面端):
app/services/plugin_installer.py下载 zip → 校验 URL(禁内网地址、手动跟随重定向且每跳重验)→ 安全解压(防 zip slip、大小/数量上限)→ 立即生效无需重启;本地插件须先禁用再卸载,内置插件不可卸载。
BasePlugin 生命周期
| 钩子 | 时机 | 用途 |
|---|---|---|
initialize() | 应用启动加载插件时调用一次 | 初始化资源 |
get_routers() | 注册阶段 | 返回挂到 /api/v1/plugins/{plugin_id} 的 FastAPI 路由 |
register_mcp_tools(mcp) | 注册阶段 | 在共享 MCP 实例上注册工具/资源 |
on_enable(user_id) | 某用户启用插件时 | 用户级初始化 |
on_disable(user_id) | 某用户禁用插件时 | 用户级清理 |
run_sync(user, db) | 定时任务或手动「立即同步」触发 | 后台同步入口,返回含 created/skipped 的结果 |
权限声明口径
枚举口径共七项,定义在 app/plugins/base.py 的 KNOWN_PERMISSIONS,完整说明见 app/plugins/PERMISSIONS.md:
| 权限 | 含义 |
|---|---|
files.read | 读本地文件 |
files.write | 写本地文件 |
network.outbound | 发起出站网络请求 |
llm.call | 调用 LLM(产生 token 消耗) |
storage.read | 读应用数据 |
storage.write | 写应用数据 |
mcp.expose | 通过 MCP 向外部 AI Agent 暴露工具 |
校验规则(PluginManifest._check_permissions):未声明按空集合处理并警告日志;声明了未知权限不拒绝安装(前向兼容,新版本权限在旧后端上仍可安装),仅在校验时警告。
内置插件
| 插件 | 默认状态 | 声明权限 |
|---|---|---|
MCP Server(app/plugins/builtin/mcp_server/) | 启用 | storage.read, storage.write, mcp.expose |
Notion 导入(app/plugins/builtin/notion_import/) | 禁用 | network.outbound, storage.write |
Pocket 同步(app/plugins/builtin/pocket_sync/) | 禁用 | network.outbound, storage.write |
Readwise 同步(app/plugins/builtin/readwise_sync/) | 禁用 | network.outbound, storage.write |
为什么是声明制而不是沙箱
MCP 生态与各类「技能沙箱」方案默认插件来自不可信来源,因此需要容器、seccomp、权限代理等重装备做运行时隔离。Molore的场景不同:本地优先意味着插件只运行在用户自己的机器上、处理用户自己的数据,没有多租户混部,也没有第三方插件市场分发链。在这个前提下:
| 维度 | 沙箱类方案 | Molore插件系统 |
|---|---|---|
| 威胁模型 | 多租户/不可信分发,插件即攻击面 | 单机单用户,插件由用户自行选择安装 |
| 隔离成本 | 容器/进程隔离,常驻额外资源 | 同进程运行,零额外资源 |
| 权限机制 | 运行时强制拦截 | manifest 声明 + 展示,不拦截 |
| 失效兜底 | 沙箱拦截违规调用 | 用户启用前审阅声明;只安装可信来源 |
项目口径是「防君子不防小人」——会员门(见 平台管理)与插件权限同哲学:声明的价值在于可读可查、启用前可决策,超声明能力属于违反约定的行为而非技术事故。代价与边界同样明确:插件能力等同于后端本身,只安装可信来源的包是唯一的硬前提。
下一步
- 插件使用与配置:用户视角的安装、启停与自动同步操作
- 平台管理与系统管理员:会员门控同源的「防君子不防小人」口径
- 可观测性与运维:插件同步失败时的排障入口