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:
@@ -31,7 +31,7 @@
|
|||||||
## 代码结构 / 平台插件
|
## 代码结构 / 平台插件
|
||||||
|
|
||||||
- 业务代码都在 `src/study_tool/` 包里;根目录只放启动壳 `app.py`、`pyproject.toml`、`static/`、`data/`、`script/` 这类运行时资源。散落的 JS 脚本统一收进 `script/`,不参与运行的参考脚本放 `script/reference/`。
|
- 业务代码都在 `src/study_tool/` 包里;根目录只放启动壳 `app.py`、`pyproject.toml`、`static/`、`data/`、`script/` 这类运行时资源。散落的 JS 脚本统一收进 `script/`,不参与运行的参考脚本放 `script/reference/`。
|
||||||
- 平台代码有两个落点:插件式平台放 `src/study_tool/pugins/<名字>/`(`pugin.json` 只放 `id` / `title` / `name` 注册信息,`main.py` 放 `Platform` 子类,`load_plugins()` 扫目录 import,新增插件不用改任何 Python 代码);跟共享实现模块关系紧密的平台放 `src/study_tool/platforms/<名字>.py` 并在 `load_builtin()` 里 import。能力只能通过「在子类里实现同名方法」绑定,禁止再写 `Platform(id=..., study=..., session=...)` 这种逐项传函数的注册;模块末尾 `PLATFORM = registry.register(<子类>())`,一个平台只注册一次。同一平台的不同页面代际可以拆成实现模块(如莞易学的 `jspx.py` / `study.py`),由平台模块按 `course["generation"]` 分发。
|
- 平台代码有两个落点:插件式平台放 `src/study_tool/pugins/<名字>/`(`pugin.json` 只放 `id` / `title` / `name` 注册信息,`main.py` 放 `Platform` 子类,`load_plugins()` 扫目录 import,新增插件不用改任何 Python 代码;`GET /api/plugins` 看清单,`POST /api/plugins/{name}/enable|disable` 启停,`POST /api/plugins/reload` 热重载);跟共享实现模块关系紧密的平台放 `src/study_tool/platforms/<名字>.py` 并在 `load_builtin()` 里 import。能力只能通过「在子类里实现同名方法」绑定,禁止再写 `Platform(id=..., study=..., session=...)` 这种逐项传函数的注册;模块末尾 `PLATFORM = registry.register(<子类>())`,一个平台只注册一次。同一平台的不同页面代际可以拆成实现模块(如莞易学的 `jspx.py` / `study.py`),由平台模块按 `course["generation"]` 分发。
|
||||||
- 新增平台的标准步骤:建 `pugins/<名字>/`,写 `pugin.json`(`id` / `title` / `name`)和 `main.py`(`class X(Platform)`,声明 `id` / `label` / `entries`,实现 `study(self, key, s, d, **opts)`,需要登录 / 课程列表 / 发现时再实现 `session` / `dump_cookies` / `login` / `list_courses` / `discover` / `renew_session` / `profile` 对应方法,没有的阶段不写,末尾 `PLATFORM = registry.register(X())`)→ 启动即可,不用改 `load_builtin()`、`engine.py`、`routes/`、前端。
|
- 新增平台的标准步骤:建 `pugins/<名字>/`,写 `pugin.json`(`id` / `title` / `name`)和 `main.py`(`class X(Platform)`,声明 `id` / `label` / `entries`,实现 `study(self, key, s, d, **opts)`,需要登录 / 课程列表 / 发现时再实现 `session` / `dump_cookies` / `login` / `list_courses` / `discover` / `renew_session` / `profile` 对应方法,没有的阶段不写,末尾 `PLATFORM = registry.register(X())`)→ 启动即可,不用改 `load_builtin()`、`engine.py`、`routes/`、前端。
|
||||||
- 引擎、路由、前端都不允许直接 import 某个具体平台模块;需要平台能力时用 `registry.get(course["platform"])`;账号级操作(登录 / 刷新课程 / 发现课程 / 会话)按 `accounts.platform` 用 `registry.get()` 取 `Platform` 上的 `session` / `dump_cookies` / `login` / `list_courses` / `discover` / `renew_session` / `profile`;「添加账号」的平台下拉来自 `GET /api/platforms`。
|
- 引擎、路由、前端都不允许直接 import 某个具体平台模块;需要平台能力时用 `registry.get(course["platform"])`;账号级操作(登录 / 刷新课程 / 发现课程 / 会话)按 `accounts.platform` 用 `registry.get()` 取 `Platform` 上的 `session` / `dump_cookies` / `login` / `list_courses` / `discover` / `renew_session` / `profile`;「添加账号」的平台下拉来自 `GET /api/platforms`。
|
||||||
- 前端源码在 `web/`(React + TypeScript + Vite + Tailwind CSS),构建产物输出到 `static/` 并提交进仓库;改完前端必须执行 `cd web && pnpm build`,不要直接改 `static/` 里的产物。
|
- 前端源码在 `web/`(React + TypeScript + Vite + Tailwind CSS),构建产物输出到 `static/` 并提交进仓库;改完前端必须执行 `cd web && pnpm build`,不要直接改 `static/` 里的产物。
|
||||||
|
|||||||
@@ -2,6 +2,30 @@
|
|||||||
|
|
||||||
已实现、但还需要你实测确认的事项。确认没问题后把对应条目删掉。
|
已实现、但还需要你实测确认的事项。确认没问题后把对应条目删掉。
|
||||||
|
|
||||||
|
## 插件热加载 + 启用/停用清单
|
||||||
|
|
||||||
|
**状态**:已实现,待实测确认。
|
||||||
|
|
||||||
|
### 做了什么
|
||||||
|
|
||||||
|
- `pugins/__init__.py` 重写:`load_plugins()` / `discover()` / `set_enabled()` / `reload()`。
|
||||||
|
- 热加载:把 `study_tool.pugins.<名字>`(含子模块)从 `sys.modules` 摘掉后重新 import,
|
||||||
|
import 时 `registry.register()` 覆盖;重载前先 `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, "error": ...}`。
|
||||||
|
- 引擎 / 路由 / 前端 / 平台实现均未改动。
|
||||||
|
|
||||||
|
### 怎么测
|
||||||
|
|
||||||
|
1. `GET /api/plugins` 列出 `gdgxk`(enabled / loaded 为 true,capabilities 七项)。
|
||||||
|
2. `POST /api/plugins/gdgxk/disable` → `GET /api/platforms` 里 gdgxk 消失;再 enable 回来。
|
||||||
|
3. 改 `pugins/gdgxk/main.py` 里一句日志文案 → `POST /api/plugins/gdgxk/reload` →
|
||||||
|
不重启服务再跑一次学习,日志应变成新文案。
|
||||||
|
4. 重启服务:停用状态应被记住(设置项 `plugins`)。
|
||||||
|
|
||||||
## 平台契约改为子类实现(注册方式调整)
|
## 平台契约改为子类实现(注册方式调整)
|
||||||
|
|
||||||
**状态**:已实现,待实测确认。
|
**状态**:已实现,待实测确认。
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ src/study_tool/
|
|||||||
jspx.py 莞易学旧版:会话 / 登录 / 课程抓取 / 旧版学习流程
|
jspx.py 莞易学旧版:会话 / 登录 / 课程抓取 / 旧版学习流程
|
||||||
study.py 莞易学新版 SPA:StudyClient / 接口学习 / 视频流程
|
study.py 莞易学新版 SPA:StudyClient / 接口学习 / 视频流程
|
||||||
pugins/ 插件式平台(加载器扫目录,新增插件不改代码)
|
pugins/ 插件式平台(加载器扫目录,新增插件不改代码)
|
||||||
__init__.py load_plugins():扫 pugins/*/pugin.json 并 import main.py
|
__init__.py load_plugins / discover / set_enabled / reload(启停 + 热重载)
|
||||||
gdgxk/
|
gdgxk/
|
||||||
pugin.json 注册信息:id / title / name(只放元数据)
|
pugin.json 注册信息:id / title / name(只放元数据)
|
||||||
main.py 广东公需课:SSO 换票 + 视频进度心跳
|
main.py 广东公需课:SSO 换票 + 视频进度心跳
|
||||||
|
|||||||
+31
-17
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
分层与整体结构见 [architecture.md](architecture.md),本文只讲「怎么写一个平台插件」。
|
分层与整体结构见 [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/
|
src/study_tool/pugins/
|
||||||
base.py Platform 基类:id / label / entries + 8 个槽位默认 None
|
__init__.py 加载器:load_plugins / discover / set_enabled / reload
|
||||||
contract.py 槽位 ↔ 能力名映射,capabilities() 用它推导
|
<名字>/
|
||||||
errors.py SessionExpired(会话失效,引擎据此续期 / 重登)
|
pugin.json 注册信息:id / title / name(不写能力绑定)
|
||||||
registry.py register / get / detect / all
|
main.py class <名字>(Platform)
|
||||||
detect.py 页面代际识别(legacy 旧版 / spa 新版)
|
gdgxk/ 广东公需课:pugin.json + main.py
|
||||||
guanyixue.py 平台:莞易学(jspx 登录入口 + study 学习入口,按 generation 分发)
|
|
||||||
jspx.py 同平台实现模块:旧版页面(不是插件,不注册)
|
|
||||||
study.py 同平台实现模块:新版 SPA(不是插件,不注册)
|
|
||||||
gdgxk.py 平台:广东公需课
|
|
||||||
```
|
```
|
||||||
|
|
||||||
约定:
|
约定:
|
||||||
@@ -96,6 +94,21 @@ src/study_tool/platforms/
|
|||||||
- 平台私有的工具函数放同一模块或同平台的实现模块里;跨平台共用的能力放 `base.py` /
|
- 平台私有的工具函数放同一模块或同平台的实现模块里;跨平台共用的能力放 `base.py` /
|
||||||
`contract.py` / `errors.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)`:
|
1. 新建 `src/study_tool/pugins/<名字>/`,放两个文件:`pugin.json`(`id` / `title` / `name`)
|
||||||
类属性 `id` / `label` / `entries`,按需实现槽位方法。
|
和 `main.py`(按上面的骨架写 `class <名字>(Platform)`)。
|
||||||
2. 模块末尾 `PLATFORM = registry.register(<名字>())`。
|
2. `main.py` 末尾 `PLATFORM = registry.register(<名字>())`。
|
||||||
3. 在 `platforms/__init__.py` 的 `load_builtin()` 里加一行
|
3. 不需要改 `load_builtin()`、路由或前端 —— 加载器扫目录时自动 import 各插件。
|
||||||
`from study_tool.platforms import <名字>`。
|
跟 `jspx` / `study` 这类共享实现模块关系紧密的平台,仍可以放 `platforms/<名字>.py`,
|
||||||
|
在 `load_builtin()` 里加一行 import。
|
||||||
4. 启动服务,`GET /api/platforms` 应出现该平台。
|
4. 启动服务,`GET /api/platforms` 应出现该平台。
|
||||||
5. 联调顺序:`/api/login` → `/api/courses` → `/api/discover` → `/api/study`。
|
5. 联调顺序:`/api/login` → `/api/courses` → `/api/discover` → `/api/study`。
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ def register(platform: Platform) -> Platform:
|
|||||||
return platform
|
return platform
|
||||||
|
|
||||||
|
|
||||||
|
def unregister(pid):
|
||||||
|
"""停用插件 / 重载前把实现从注册表摘掉;不在表里返回 None。"""
|
||||||
|
return _PLATFORMS.pop(pid or "", None)
|
||||||
|
|
||||||
|
|
||||||
def get(pid):
|
def get(pid):
|
||||||
"""按 course["platform"] 取平台实现。"""
|
"""按 course["platform"] 取平台实现。"""
|
||||||
return _PLATFORMS.get(pid or "")
|
return _PLATFORMS.get(pid or "")
|
||||||
|
|||||||
@@ -3,24 +3,131 @@
|
|||||||
pugin.json 注册信息:id / title / name(只写元数据,不写任何能力绑定)
|
pugin.json 注册信息:id / title / name(只写元数据,不写任何能力绑定)
|
||||||
main.py `Platform` 子类,import 时用 `registry.register(...)` 注册自己
|
main.py `Platform` 子类,import 时用 `registry.register(...)` 注册自己
|
||||||
|
|
||||||
加载 = 扫目录逐个 import `main.py`。插件里不用声明「哪个函数绑定哪个槽位」,
|
对外四个函数(路由 `/api/plugins*` 直接用):
|
||||||
子类实现了哪些方法就具备哪些能力(映射见 `platforms/contract.py`)。
|
|
||||||
要换加载方式,只改这里的 `load_plugins()`。
|
load_plugins() 启动时按启用清单加载全部插件
|
||||||
|
discover() 磁盘清单 + 启用 / 已加载状态(不 import 插件代码)
|
||||||
|
set_enabled(name, bool) 启用 / 停用,立即生效并落库
|
||||||
|
reload(name=None) 重新 import(改完插件代码不用重启服务)
|
||||||
|
|
||||||
|
启用清单存在设置项 `plugins` 里(`{"disabled": [...]}`):没列进去的默认启用,
|
||||||
|
新丢进目录的插件会自动生效;停用的插件从注册表摘掉,`/api/platforms` 里不再出现。
|
||||||
"""
|
"""
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from study_tool import runtime, store
|
||||||
from study_tool.platforms import registry
|
from study_tool.platforms import registry
|
||||||
|
|
||||||
HERE = Path(__file__).resolve().parent
|
HERE = Path(__file__).resolve().parent
|
||||||
|
SETTING = "plugins"
|
||||||
|
_LOADED = {} # 插件名 -> 注册时用的平台 id,停用 / 重载时据此摘下旧实现
|
||||||
|
|
||||||
|
|
||||||
|
def _meta(name):
|
||||||
|
meta = HERE / name / "pugin.json"
|
||||||
|
if not meta.is_file():
|
||||||
|
raise RuntimeError("没有这个插件:%s" % name)
|
||||||
|
return json.loads(meta.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def _disabled():
|
||||||
|
return set((store.get_setting(SETTING) or {}).get("disabled") or [])
|
||||||
|
|
||||||
|
|
||||||
|
def _save_disabled(names):
|
||||||
|
store.set_setting(SETTING, {"disabled": sorted(names)})
|
||||||
|
|
||||||
|
|
||||||
|
def _forget(name):
|
||||||
|
"""把该插件的模块(含子模块)从 sys.modules 摘掉,下次 import 会重新执行。"""
|
||||||
|
pkg = "%s.%s" % (__name__, name)
|
||||||
|
for mod in [m for m in sys.modules if m == pkg or m.startswith(pkg + ".")]:
|
||||||
|
del sys.modules[mod]
|
||||||
|
|
||||||
|
|
||||||
|
def _load(name):
|
||||||
|
"""import 一个插件并确认它注册成功;重复调用就是重新加载。"""
|
||||||
|
pid = _meta(name).get("id") or ""
|
||||||
|
_unload(name) # 旧实现先下线,id 改了也不会留下残影
|
||||||
|
importlib.import_module("%s.%s.main" % (__name__, name))
|
||||||
|
platform = registry.get(pid)
|
||||||
|
if platform is None:
|
||||||
|
raise RuntimeError("插件 %s 没有注册 id=%r" % (name, pid))
|
||||||
|
_LOADED[name] = pid
|
||||||
|
return platform
|
||||||
|
|
||||||
|
|
||||||
|
def _unload(name):
|
||||||
|
pid = _LOADED.pop(name, None)
|
||||||
|
if pid:
|
||||||
|
registry.unregister(pid)
|
||||||
|
_forget(name)
|
||||||
|
|
||||||
|
|
||||||
|
def discover():
|
||||||
|
"""磁盘上的插件清单;不 import 插件代码。"""
|
||||||
|
disabled = _disabled()
|
||||||
|
out = []
|
||||||
|
for meta in sorted(HERE.glob("*/pugin.json")):
|
||||||
|
name = meta.parent.name
|
||||||
|
info = json.loads(meta.read_text(encoding="utf-8"))
|
||||||
|
platform = registry.get(info.get("id") or "")
|
||||||
|
out.append({
|
||||||
|
"name": name,
|
||||||
|
"id": info.get("id") or "",
|
||||||
|
"title": info.get("title") or "",
|
||||||
|
"enabled": name not in disabled,
|
||||||
|
"loaded": platform is not None,
|
||||||
|
"label": platform.label if platform else "",
|
||||||
|
"capabilities": platform.capabilities() if platform else [],
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def load_plugins():
|
def load_plugins():
|
||||||
"""导入 `pugins/` 下的全部插件(导入即注册)。幂等,app 启动时调用一次。"""
|
"""启动时加载启用清单里的全部插件。幂等。"""
|
||||||
for meta in sorted(HERE.glob("*/pugin.json")):
|
for item in discover():
|
||||||
info = json.loads(meta.read_text(encoding="utf-8"))
|
if not item["enabled"]:
|
||||||
name = meta.parent.name
|
continue
|
||||||
importlib.import_module("%s.%s.main" % (__name__, name))
|
try:
|
||||||
if not registry.get(info.get("id") or ""):
|
_load(item["name"])
|
||||||
raise RuntimeError("插件 %s 没有注册 id=%r" % (name, info.get("id")))
|
except Exception as e:
|
||||||
|
runtime.log("system", "插件 %s 加载失败:%s" % (item["name"], e), level="warn")
|
||||||
|
|
||||||
|
|
||||||
|
def set_enabled(name, enabled):
|
||||||
|
"""启用 / 停用插件并立即生效;返回最新的插件清单。"""
|
||||||
|
if enabled:
|
||||||
|
platform = _load(name) # 先加载,失败了不写进启用清单
|
||||||
|
disabled = _disabled()
|
||||||
|
disabled.discard(name)
|
||||||
|
_save_disabled(disabled)
|
||||||
|
runtime.log("system", "插件已启用:%s(%s)" % (name, platform.label))
|
||||||
|
else:
|
||||||
|
disabled = _disabled()
|
||||||
|
disabled.add(name)
|
||||||
|
_save_disabled(disabled)
|
||||||
|
_unload(name)
|
||||||
|
runtime.log("system", "插件已停用:%s" % name)
|
||||||
|
return discover()
|
||||||
|
|
||||||
|
|
||||||
|
def reload(name=None):
|
||||||
|
"""重新 import 插件;name 为空 = 重载所有已启用的插件。"""
|
||||||
|
items = discover()
|
||||||
|
if name:
|
||||||
|
item = next((i for i in items if i["name"] == name), None)
|
||||||
|
if item is None:
|
||||||
|
raise RuntimeError("没有这个插件:%s" % name)
|
||||||
|
if not item["enabled"]:
|
||||||
|
raise RuntimeError("插件 %s 已停用,先启用再重载" % name)
|
||||||
|
names = [name]
|
||||||
|
else:
|
||||||
|
names = [i["name"] for i in items if i["enabled"]]
|
||||||
|
for n in names:
|
||||||
|
_load(n)
|
||||||
|
runtime.log("system", "插件已重载:%s" % n)
|
||||||
|
return discover()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"""课程 / 发现 / 学习 / 停止 / 抓包 / 快照 / 历史日志接口。"""
|
"""课程 / 发现 / 学习 / 停止 / 抓包 / 快照 / 历史日志 / 插件管理接口。"""
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
from study_tool.webapp import app
|
from study_tool.webapp import app
|
||||||
|
|
||||||
from study_tool import store
|
from study_tool import pugins, store
|
||||||
from study_tool.capture import *
|
from study_tool.capture import *
|
||||||
from study_tool.config import *
|
from study_tool.config import *
|
||||||
from study_tool.engine import study_worker
|
from study_tool.engine import study_worker
|
||||||
@@ -25,6 +25,44 @@ def api_platforms():
|
|||||||
for p in registry.all()]}
|
for p in registry.all()]}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ 插件管理
|
||||||
|
# 插件目录 pugins/<名字>/,实现见 study_tool.pugins。
|
||||||
|
|
||||||
|
@app.get("/api/plugins")
|
||||||
|
def api_plugins():
|
||||||
|
"""磁盘上的插件清单:是否启用、是否已加载、加载后的 label 与 capabilities。"""
|
||||||
|
return {"ok": True, "plugins": pugins.discover()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/plugins/{name}/enable")
|
||||||
|
def api_plugin_enable(name: str):
|
||||||
|
return _plugin_call(lambda: pugins.set_enabled(name, True))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/plugins/{name}/disable")
|
||||||
|
def api_plugin_disable(name: str):
|
||||||
|
return _plugin_call(lambda: pugins.set_enabled(name, False))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/plugins/reload")
|
||||||
|
def api_plugins_reload():
|
||||||
|
"""重载全部已启用插件:改完插件代码不用重启服务。"""
|
||||||
|
return _plugin_call(pugins.reload)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/plugins/{name}/reload")
|
||||||
|
def api_plugin_reload(name: str):
|
||||||
|
return _plugin_call(lambda: pugins.reload(name))
|
||||||
|
|
||||||
|
|
||||||
|
def _plugin_call(fn):
|
||||||
|
"""插件操作失败不抛 500,返回 ok=false 给前端提示。"""
|
||||||
|
try:
|
||||||
|
return {"ok": True, "plugins": fn()}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "error": str(e), "plugins": pugins.discover()}
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ 运行日志 / 快照
|
# ------------------------------------------------------------------ 运行日志 / 快照
|
||||||
|
|
||||||
@app.get("/api/log/history")
|
@app.get("/api/log/history")
|
||||||
|
|||||||
Reference in New Issue
Block a user