feat(plugins): 插件支持热加载与启用/停用清单

- pugins/__init__.py 重写:load_plugins / discover / set_enabled / reload
- 热加载:把 study_tool.pugins.<名字> 的模块从 sys.modules 摘掉后重新 import;
  重载前先 registry.unregister 旧 id,平台 id 改了也不会留残影
- 启用清单存设置项 plugins({"disabled": [...]}):未列出的默认启用,新插件自动生效;
  停用即从注册表摘掉,/api/platforms 与账号平台下拉同步消失
- platforms/registry.py 新增 unregister(pid)
- 新接口:GET /api/plugins、POST /api/plugins/{name}/enable|disable、
  POST /api/plugins/reload、POST /api/plugins/{name}/reload;失败返回 ok=false
- 同步 docs/pugin.md(新增「加载与启停」)、docs/architecture.md、docs/OPTIMIZATION.md、AGENTS.md
This commit is contained in:
study-tool
2026-10-31 15:50:06 +08:00
parent f1a57853f4
commit f8458cff51
7 changed files with 219 additions and 31 deletions
+31 -17
View File
@@ -2,8 +2,10 @@
分层与整体结构见 [architecture.md](architecture.md),本文只讲「怎么写一个平台插件」。
一个平台 = `src/study_tool/platforms/<平台>.py` 里的一个 `Platform` 子类:类属性放元数据,
自己确实有的阶段实现成方法,模块末尾注册一次。引擎、路由、前端都不需要改。
一个平台 = 一个 `Platform` 子类:类属性放元数据,自己确实有的阶段实现成方法,模块末尾
注册一次。插件式平台放 `src/study_tool/pugins/<名字>/`(`pugin.json` + `main.py`),由加载器
扫目录自动加载;跟共享实现模块关系紧密的放 `src/study_tool/platforms/<名字>.py`。
引擎、路由、前端都不需要改。
## 插件骨架
@@ -75,16 +77,12 @@ PLATFORM = registry.register(MyPlatform())
## 目录结构
```
src/study_tool/platforms/
base.py Platform 基类:id / label / entries + 8 个槽位默认 None
contract.py 槽位 ↔ 能力名映射,capabilities() 用它推导
errors.py SessionExpired(会话失效,引擎据此续期 / 重登)
registry.py register / get / detect / all
detect.py 页面代际识别(legacy 旧版 / spa 新版)
guanyixue.py 平台:莞易学(jspx 登录入口 + study 学习入口,按 generation 分发)
jspx.py 同平台实现模块:旧版页面(不是插件,不注册)
study.py 同平台实现模块:新版 SPA(不是插件,不注册)
gdgxk.py 平台:广东公需课
src/study_tool/pugins/
__init__.py 加载器:load_plugins / discover / set_enabled / reload
<名字>/
pugin.json 注册信息:id / title / name(不写能力绑定)
main.py class <名字>(Platform)
gdgxk/ 广东公需课:pugin.json + main.py
```
约定:
@@ -96,6 +94,21 @@ src/study_tool/platforms/
- 平台私有的工具函数放同一模块或同平台的实现模块里;跨平台共用的能力放 `base.py` /
`contract.py` / `errors.py`。
## 加载与启停
加载器在 `pugins/__init__.py`,对外四个函数 + 对应 HTTP 接口:
| 函数 | 接口 | 作用 |
| --- | --- | --- |
| `load_plugins()` | 启动时自动调用 | 按启用清单 import 全部插件 |
| `discover()` | `GET /api/plugins` | 磁盘清单 + 是否启用 / 已加载 / label / capabilities |
| `set_enabled(name, bool)` | `POST /api/plugins/{name}/enable`、`/disable` | 立即加载或从注册表摘掉,并落库 |
| `reload(name=None)` | `POST /api/plugins/reload`、`/api/plugins/{name}/reload` | 重新 import,改完插件代码不用重启 |
启用清单存在设置项 `plugins`(`{"disabled": [...]}`):没列进去的默认启用,新丢进目录的插件自动生效。
停用的插件会 `registry.unregister()`,从 `/api/platforms` 消失,账号的平台下拉里也不会出现。
热重载先把旧实现从注册表摘掉再 import;正在跑的学习任务持有的是旧实例,跑到结束不受影响。
## 槽位契约
| 槽位 | 方法签名 | 调用方 | 要求 |
@@ -121,11 +134,12 @@ quiz_key / cookies / plan / project / name`,插件只取自己需要的。
## 开发流程
1. 新建 `src/study_tool/platforms/<名字>.py`,按上面的骨架写 `class <名字>(Platform)`:
类属性 `id` / `label` / `entries`,按需实现槽位方法。
2. 模块末尾 `PLATFORM = registry.register(<名字>())`。
3. 在 `platforms/__init__.py` 的 `load_builtin()` 里加一行
`from study_tool.platforms import <名字>`。
1. 新建 `src/study_tool/pugins/<名字>/`,放两个文件:`pugin.json`(`id` / `title` / `name`)
和 `main.py`(按上面的骨架写 `class <名字>(Platform)`)。
2. `main.py` 末尾 `PLATFORM = registry.register(<名字>())`。
3. 不需要改 `load_builtin()`、路由或前端 —— 加载器扫目录时自动 import 各插件。
跟 `jspx` / `study` 这类共享实现模块关系紧密的平台,仍可以放 `platforms/<名字>.py`,
在 `load_builtin()` 里加一行 import。
4. 启动服务,`GET /api/platforms` 应出现该平台。
5. 联调顺序:`/api/login` → `/api/courses` → `/api/discover` → `/api/study`。