refactor(platforms): 平台能力改为子类实现,注册不再逐项绑定函数

- Platform 由 dataclass 字段绑定改为基类:id / label / entries 是类属性,能力是子类方法,
  未实现的阶段保持基类 None,capabilities() 与 /api/platforms 输出不变
- gdgxk:session(原 new_session)/ login / list_courses / discover / study / _relogin
  收进 class Gdgxk(Platform),解析与上报 helper 仍是模块级函数
- guanyixue:改成 class Guanyixue(Platform),方法转调 jspx / study
- 删除 src/study_tool/pugins/ 草稿目录(内容与 platforms/gdgxk.py 重复,
  pugin.json 里按名字声明 study / entries 的绑定方式作废,元数据改由类属性承载)
- 引擎 / 路由 / quiz / contract / 前端未改动
- 同步 docs/architecture.md、AGENTS.md、docs/OPTIMIZATION.md
This commit is contained in:
study-tool
2026-10-31 15:02:21 +08:00
parent b1ccfe89f6
commit f999e4fa4d
7 changed files with 293 additions and 241 deletions
+34 -25
View File
@@ -61,24 +61,24 @@ pf = registry.get(d.get("platform"))
pf.study(key, s, d, workers=..., cap=..., enable_quiz=..., ...)
```
`Platform` 契约(见 `platforms/base.py`):
`Platform` 契约(见 `platforms/base.py`):一个平台 = 一个 `Platform` 子类,类属性放元数据,
自己确实有的阶段实现成方法;注册时只实例化子类,不再逐项绑定函数。
```python
@dataclass
class Platform:
id: str # course["platform"] 的取值
label: str # 日志 / 界面展示名,如「莞易学」
study: Callable # (key, s, d, **opts) -> None
entries: tuple[str, ...] # 登录入口 / 学习入口的 URL 片段
# 账号级能力:添加账号时选中的平台决定登录 / 刷新课程 / 发现课程走哪套实现
session: Callable # (cookies) -> requests.Session
dump_cookies: Callable # (session) -> [cookie]
login: Callable # (username, password) -> (session, user)
list_courses: Callable # (session) -> [course]
discover: Callable # (session, plan_id, project_id, name) -> 课程描述
renew_session: Callable # (session) -> bool,无密码续期
profile: Callable # (session) -> 显示名
# 槽位为 None = 该平台没有这项能力;capabilities() 按非空槽位推导
id = "" # course["platform"] 的取值
label = "" # 日志 / 界面展示名,如「莞易学」
entries = () # 登录入口 / 学习入口的 URL 片段
# 槽位:子类实现哪个阶段就覆盖成方法(第一个参数是 self);保持 None = 该平台没有这项能力
study = None # study(self, key, s, d, **opts) -> None
session = None # session(self, cookies) -> requests.Session
dump_cookies = None # dump_cookies(self, session) -> [cookie]
login = None # login(self, username, password) -> (session, user)
list_courses = None # list_courses(self, session) -> [course]
discover = None # discover(self, session, plan_id, project_id, name) -> 课程描述
renew_session = None # renew_session(self, session) -> bool,无密码续期
profile = None # profile(self, session) -> 显示名
# capabilities() 按非空槽位推导能力名
```
`registry.detect(url, html)` 按 `entries` 命中平台;「添加账号」时选择的平台写进
@@ -130,19 +130,28 @@ quiz_key / cookies / plan / project / name`。插件可以只取自己需要的
from study_tool.platforms import registry
from study_tool.platforms.base import Platform
def _study(key, s, d, **opts):
# 用 s(requests.Session,带该账号 cookie)把课程活动跑完
# 日志一律 runtime.log(key, ...)
...
class MyPlatform(Platform):
id = "<平台 id>"
label = "展示名"
entries = ("<登录入口域名>", "<学习入口域名>")
PLATFORM = registry.register(Platform(
id="<平台 id>", label="展示名", study=_study,
entries=("<登录入口域名>", "<学习入口域名>"),
session=<new_session>, dump_cookies=<dump_cookies>, login=<login>,
list_courses=<list_courses>, discover=<discover>,
))
def session(self, cookies=None): ...
def dump_cookies(self, s): ...
def login(self, username, password): ...
def list_courses(self, s): ...
def discover(self, s, plan_id, project_id, name=""): ...
def profile(self, s): ...
def study(self, key, s, d, **opts):
# 用 s(requests.Session,带该账号 cookie)把课程活动跑完
# 日志一律 runtime.log(key, ...)
...
PLATFORM = registry.register(MyPlatform())
```
平台确实没有的阶段不要写,保持基类的 `None`,引擎按能力判断走不走那个分支。
2. 在 `platforms/__init__.py` 的 `load_builtin()` 里加一行 `from study_tool.platforms import <名字>`。
3. 平台注册后会自动出现在「添加账号」的平台下拉框;账号选了该平台,登录 / 刷新课程 /