refactor(plugins): gdgxk 迁到 pugins/gdgxk,插件由目录扫描加载
- platforms/gdgxk.py → pugins/gdgxk/main.py(实现不变,仍是 class Gdgxk(Platform)) - pugins/gdgxk/pugin.json 只放注册信息 id / title / name,不声明任何能力绑定 - pugins/__init__.py:load_plugins() 扫 pugins/*/pugin.json、import 各自的 main.py, 再用 pugin.json 的 id 校验注册结果;要换加载方式只改这一个函数 - platforms/__init__.py::load_builtin() 调 load_plugins();guanyixue 依赖 jspx/study, 暂留 platforms/ - engine / routes / quiz / contract / 前端未改动 - 同步 AGENTS.md、docs/architecture.md、docs/OPTIMIZATION.md
This commit is contained in:
@@ -31,8 +31,8 @@
|
|||||||
## 代码结构 / 平台插件
|
## 代码结构 / 平台插件
|
||||||
|
|
||||||
- 业务代码都在 `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/platforms/` 下;一个平台 = 一个 `Platform` 子类(类属性 `id` / `label` / `entries`),能力只能通过「在子类里实现同名方法」绑定,禁止再写 `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 代码);跟共享实现模块关系紧密的平台放 `src/study_tool/platforms/<名字>.py` 并在 `load_builtin()` 里 import。能力只能通过「在子类里实现同名方法」绑定,禁止再写 `Platform(id=..., study=..., session=...)` 这种逐项传函数的注册;模块末尾 `PLATFORM = registry.register(<子类>())`,一个平台只注册一次。同一平台的不同页面代际可以拆成实现模块(如莞易学的 `jspx.py` / `study.py`),由平台模块按 `course["generation"]` 分发。
|
||||||
- 新增平台的标准步骤:写 `platforms/<名字>.py`(`class X(Platform)`,声明 `id` / `label` / `entries`,实现 `study(self, key, s, d, **opts)`,需要登录 / 课程列表 / 发现时再实现 `session` / `dump_cookies` / `login` / `list_courses` / `discover` / `renew_session` / `profile` 对应方法,没有的阶段不写)→ 在 `platforms/__init__.py` 的 `load_builtin()` 里 import → 结束。不要改 `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/` 里的产物。
|
||||||
- 新代码的 import 一律显式(如 `from study_tool.config import JSPX`),不要再新增 `from xxx import *`;历史遗留的 star import 只保留在拆分前就存在的模块里。
|
- 新代码的 import 一律显式(如 `from study_tool.config import JSPX`),不要再新增 `from xxx import *`;历史遗留的 star import 只保留在拆分前就存在的模块里。
|
||||||
|
|||||||
@@ -10,11 +10,13 @@
|
|||||||
|
|
||||||
- `Platform` 从 dataclass「注册时逐项传函数」改成基类:`id` / `label` / `entries` 是类属性,
|
- `Platform` 从 dataclass「注册时逐项传函数」改成基类:`id` / `label` / `entries` 是类属性,
|
||||||
能力是子类方法;没实现的阶段保持基类的 `None`,`capabilities()` 与 `/api/platforms` 不变。
|
能力是子类方法;没实现的阶段保持基类的 `None`,`capabilities()` 与 `/api/platforms` 不变。
|
||||||
- `platforms/gdgxk.py`:`session`(原 `new_session`)/ `login` / `list_courses` / `discover` /
|
- `pugins/gdgxk/main.py`(原 `platforms/gdgxk.py`):`session`(原 `new_session`)/ `login` /
|
||||||
`study` 等收进 `class Gdgxk(Platform)`;解析 / 上报类 helper 仍是模块级函数。
|
`list_courses` / `discover` / `study` 等收进 `class Gdgxk(Platform)`;解析 / 上报类 helper
|
||||||
|
仍是模块级函数。
|
||||||
- `platforms/guanyixue.py`:改成 `class Guanyixue(Platform)`,方法转调 `jspx` / `study`。
|
- `platforms/guanyixue.py`:改成 `class Guanyixue(Platform)`,方法转调 `jspx` / `study`。
|
||||||
- 删掉 `src/study_tool/pugins/` 草稿目录:它和 `platforms/gdgxk.py` 内容重复,
|
- `pugins/` 改成插件目录:`pugins/<名字>/pugin.json`(只放 `id` / `title` / `name`)+ `main.py`
|
||||||
`pugin.json` 里「按名字声明 `study` / `entries`」的绑定方式作废,元数据改为类属性。
|
(`Platform` 子类);`pugins/__init__.py::load_plugins()` 扫目录 import,`load_builtin()` 调它。
|
||||||
|
插件里不再声明任何能力绑定。
|
||||||
- 引擎 / 路由 / `quiz.py` / 前端 / `platforms/contract.py` 均未改动。
|
- 引擎 / 路由 / `quiz.py` / 前端 / `platforms/contract.py` 均未改动。
|
||||||
- 新增 [docs/pugin.md](plugin.md):插件骨架、槽位契约、新增与迁移流程、常见坑。
|
- 新增 [docs/pugin.md](plugin.md):插件骨架、槽位契约、新增与迁移流程、常见坑。
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -19,7 +19,7 @@ src/study_tool/
|
|||||||
quiz.py 测验解析 / 交卷 / AI 答题(含 /api/quiz/*)
|
quiz.py 测验解析 / 交卷 / AI 答题(含 /api/quiz/*)
|
||||||
engine.py 账号级编排:课程循环、会话续期、按 platform 分发
|
engine.py 账号级编排:课程循环、会话续期、按 platform 分发
|
||||||
capture.py 抓包
|
capture.py 抓包
|
||||||
platforms/ 平台插件(见下)
|
platforms/ 平台运行时(见下)
|
||||||
base.py Platform 契约(id / label / 槽位 / capabilities)
|
base.py Platform 契约(id / label / 槽位 / capabilities)
|
||||||
contract.py 生命周期 ↔ 槽位 ↔ 能力名映射
|
contract.py 生命周期 ↔ 槽位 ↔ 能力名映射
|
||||||
errors.py SessionExpired(平台层错误类型)
|
errors.py SessionExpired(平台层错误类型)
|
||||||
@@ -28,7 +28,11 @@ src/study_tool/
|
|||||||
guanyixue.py 莞易学平台:合并 jspx 登录入口与 study 学习入口
|
guanyixue.py 莞易学平台:合并 jspx 登录入口与 study 学习入口
|
||||||
jspx.py 莞易学旧版:会话 / 登录 / 课程抓取 / 旧版学习流程
|
jspx.py 莞易学旧版:会话 / 登录 / 课程抓取 / 旧版学习流程
|
||||||
study.py 莞易学新版 SPA:StudyClient / 接口学习 / 视频流程
|
study.py 莞易学新版 SPA:StudyClient / 接口学习 / 视频流程
|
||||||
gdgxk.py 广东公需课:SSO 换票 + 视频进度心跳
|
pugins/ 插件式平台(加载器扫目录,新增插件不改代码)
|
||||||
|
__init__.py load_plugins():扫 pugins/*/pugin.json 并 import main.py
|
||||||
|
gdgxk/
|
||||||
|
pugin.json 注册信息:id / title / name(只放元数据)
|
||||||
|
main.py 广东公需课:SSO 换票 + 视频进度心跳
|
||||||
routes/ HTTP 接口
|
routes/ HTTP 接口
|
||||||
accounts.py 账号 / 登录 / 登录记录
|
accounts.py 账号 / 登录 / 登录记录
|
||||||
misc.py 课程 / 发现 / 学习 / 停止 / 抓包 / 快照 / 平台列表
|
misc.py 课程 / 发现 / 学习 / 停止 / 抓包 / 快照 / 平台列表
|
||||||
@@ -53,7 +57,8 @@ script/reference/ 不参与运行的参考脚本:dgjy-toolkit.user.js
|
|||||||
|
|
||||||
## 平台插件与识别
|
## 平台插件与识别
|
||||||
|
|
||||||
平台实现在 `platforms/`,通过注册表注入,`engine` 只按 `course["platform"]` 分发:
|
平台实现在 `platforms/` 或插件目录 `pugins/<名字>/`,都通过注册表注入,`engine` 只按
|
||||||
|
`course["platform"]` 分发:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# engine.run_one_course 末尾
|
# engine.run_one_course 末尾
|
||||||
@@ -118,7 +123,8 @@ quiz_key / cookies / plan / project / name`。插件可以只取自己需要的
|
|||||||
|
|
||||||
### 内置:广东公需课
|
### 内置:广东公需课
|
||||||
|
|
||||||
`id=gdgxk`,`platforms/gdgxk.py` 单文件实现:在 `jsglpt.gds.edu.cn` 登录(账号、密码
|
`id=gdgxk`,`pugins/gdgxk/main.py` 实现(插件目录,`pugin.json` 只放 id / title / name):
|
||||||
|
在 `jsglpt.gds.edu.cn` 登录(账号、密码
|
||||||
都是 base64),再用 `ssoItlmsGxkTicket.action` 换票进 `jsxx.gds.edu.cn`;视频按真实
|
都是 base64),再用 `ssoItlmsGxkTicket.action` 换票进 `jsxx.gds.edu.cn`;视频按真实
|
||||||
流逝时间上报 `updateViewTime1`。没有测验 / 反馈表,所以不实现对应能力。
|
流逝时间上报 `updateViewTime1`。没有测验 / 反馈表,所以不实现对应能力。
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
"""平台插件包。
|
"""平台包。
|
||||||
|
|
||||||
内置平台在 `load_builtin()` 里导入(导入即注册)。新增平台:
|
平台实现有两个落点,写法和契约完全一样(`Platform` 子类 + 注册一次):
|
||||||
1. 新建 `platforms/<名字>.py`,写一个 `Platform` 子类:类属性声明 `id` / `label` /
|
|
||||||
`entries`,自己有的阶段实现成方法(见 `base.py`),末尾
|
- `pugins/<名字>/`:插件式平台(`pugin.json` + `main.py`),由 `study_tool.pugins.load_plugins()`
|
||||||
`PLATFORM = registry.register(<子类>())`;
|
扫描加载,新增插件不需要改这里的任何代码;
|
||||||
2. 在下面的 `load_builtin()` 里加一行 import;
|
- `platforms/<名字>.py`:跟共享实现模块(`jspx.py` / `study.py`)关系紧密的平台,直接在
|
||||||
3. 路由 / 引擎不用改 —— 它们只认 `course["platform"]`。
|
`load_builtin()` 里 import。
|
||||||
|
|
||||||
|
路由 / 引擎不用改 —— 它们只认 `course["platform"]`。
|
||||||
"""
|
"""
|
||||||
from study_tool.platforms import registry # noqa: F401 供 engine、路由使用
|
from study_tool.platforms import registry # noqa: F401 供 engine、路由使用
|
||||||
from study_tool.platforms.base import Platform # noqa: F401
|
from study_tool.platforms.base import Platform # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
def load_builtin():
|
def load_builtin():
|
||||||
"""导入内置平台,触发注册。幂等,app 启动时调用一次。"""
|
"""导入内置平台与 `pugins/` 下的插件,触发注册。幂等,app 启动时调用一次。"""
|
||||||
from study_tool.platforms import gdgxk # noqa: F401
|
|
||||||
from study_tool.platforms import guanyixue # noqa: F401
|
from study_tool.platforms import guanyixue # noqa: F401
|
||||||
|
from study_tool.pugins import load_plugins
|
||||||
|
load_plugins()
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""插件目录:`pugins/<名字>/` = 一个平台插件。
|
||||||
|
|
||||||
|
pugin.json 注册信息:id / title / name(只写元数据,不写任何能力绑定)
|
||||||
|
main.py `Platform` 子类,import 时用 `registry.register(...)` 注册自己
|
||||||
|
|
||||||
|
加载 = 扫目录逐个 import `main.py`。插件里不用声明「哪个函数绑定哪个槽位」,
|
||||||
|
子类实现了哪些方法就具备哪些能力(映射见 `platforms/contract.py`)。
|
||||||
|
要换加载方式,只改这里的 `load_plugins()`。
|
||||||
|
"""
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from study_tool.platforms import registry
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugins():
|
||||||
|
"""导入 `pugins/` 下的全部插件(导入即注册)。幂等,app 启动时调用一次。"""
|
||||||
|
for meta in sorted(HERE.glob("*/pugin.json")):
|
||||||
|
info = json.loads(meta.read_text(encoding="utf-8"))
|
||||||
|
name = meta.parent.name
|
||||||
|
importlib.import_module("%s.%s.main" % (__name__, name))
|
||||||
|
if not registry.get(info.get("id") or ""):
|
||||||
|
raise RuntimeError("插件 %s 没有注册 id=%r" % (name, info.get("id")))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""广东公需课插件:实现在 `main.py`,注册信息在 `pugin.json`。"""
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"id": "gdgxk",
|
||||||
|
"title": "广东省教师继续教育管理平台",
|
||||||
|
"name": "gdgxk"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user