Files
study-tool/docs/pugin.md
T

8.8 KiB
Raw Blame History

学习平台插件开发文档

一个学习平台 = src/study_tool/pugins/<平台 id>/ 一个目录:main.py 放实现,pugin.json 放元数据,utils/ 放这个平台自己的工具。平台 id 同时是 accounts.platform 与 course["platform"] 的取值,必须与目录名、registry 里注册的 Platform.id 三者一致。

现状(重要)

新目录方案还没有接加载器,pugins/ 下的代码目前不会被执行:

  • 启动时只调 platforms/__init__.py 的 load_builtin(),它 import 的是 platforms.gdgxk 与 platforms.guanyixue。
  • pugins/gdgxk/main.py 与 platforms/gdgxk.py 内容完全相同(副本)。
  • 没有任何代码读取 pugin.json;注册仍由 main.py 末尾的 registry.register(Platform(...)) 完成。

所以现在改 pugins/gdgxk/main.py 不生效;要改行为先改 platforms/gdgxk.py, 或者先补加载器(见文末「待定项」)。

目录结构

src/study_tool/
  platforms/                  现有实现,加载器仍指向这里
    base.py                   Platform 契约
    contract.py               生命周期 ↔ 槽位 ↔ 能力名映射
    errors.py                 SessionExpired
    registry.py               register / get / detect / all
    detect.py                 页面代际识别(legacy / spa)
    guanyixue.py  jspx.py  study.py  gdgxk.py
  pugins/                     插件目录(一个平台一个子目录)
    gdgxk/
      __init__.py             留空,仅让目录成为包
      pugin.json              插件元数据
      main.py                 实现 + registry.register(Platform(...))
      utils/
        __init__.py           该平台私有工具(可留空)
    jspx/                     预留(迁移用占位)
      __init__.py

约定:

  • 目录名 = Platform.id = accounts.platform = course["platform"]。
  • 一个平台只注册一次;注册发生在 import 时,因此插件模块被导入不能有副作用(除注册)。
  • utils/ 只放这个平台自己的代码;跨平台共用的能力放 platforms/(契约、注册表、错误类型)。
  • 同平台不同页面代际(如莞易学的旧版 / 新版)拆成同目录内的多个实现模块,由 Platform.study 按 course["generation"] 分发,不要一个代际一个插件目录。

pugin.json

{
  "title": "广东省教师继续教育管理平台",
  "name": "gdgxk",
  "id": "PLATFORM_ID",
  "label": "广东公需课",
  "study": "study",
  "entries": ["jsglpt.gds.edu.cn", "jsxx.gds.edu.cn"]
}

字段含义与现状:

字段 含义 现状
title 平台全称,给文档 / 未来的插件列表用 无代码读取
name 插件名,与目录名一致 无代码读取
id Platform.id,写进 accounts.platform 当前值是常量名 PLATFORM_ID,应该是 "gdgxk"
label 界面展示名(「添加账号」下拉) 无代码读取,实际值在 main.py 里
study study 实现所在的函数名 无代码读取
entries 登录 / 学习入口域名片段,用于识别平台归属 无代码读取,实际值在 main.py 里

注意:pugin.json 与 main.py 里的 Platform(...) 字段重复。在加载器定下来之前, 以 main.py 为准;pugin.json 只当元数据,改了它不会影响运行。

main.py 契约

实现 Platform 上的槽位,缺的阶段不实现(不要抛 NotImplementedError)。流程顺序由引擎负责, 插件只解决「这一步在这个站点怎么做」。

槽位 签名 调用方 要求
id / label 字符串 注册表、GET /api/platforms、前端下拉 必填
entries tuple[str, ...] registry.detect() 必填
study (key, s, d, **opts) engine.run_one_course 能学习则必填
discover (s, plan_id, project_id, name) run_one_course、/api/discover 学习前必填
session (cookies) -> requests.Session 引擎与各路由 强烈建议(缺失会直接报错)
dump_cookies (s) -> [cookie] 登录 / 刷新 / 发现后回写前端 强烈建议
login (username, password) -> (session, user) /api/login 要密码登录则必填
list_courses (s) -> [course] 登录后、/api/courses 要刷新课程则必填
renew_session (s) -> bool SessionExpired 时无密码续期 选填
profile (s) -> 显示名 登录后卡片显示 选填

study 收到的是 discover 的返回值(d),必须包含:

  • platform:本平台 id(引擎据此再次取实现)
  • course / progress / info / user:课程名、进度、说明、显示名
  • courseware / feedback / test:三类活动列表,各自元素至少含 title、href / period_id、done
  • 可选:generation(legacy / spa)、train_id、page_url、need_join

opts 固定包含 workers / cap / enable_quiz / enable_feedback / mode / parallel / quiz_key / cookies / plan / project / name,插件只取自己需要的。

Platform.capabilities() 按已实现的槽位推导能力名(映射见 platforms/contract.py), GET /api/platforms 会把它返回给前端。

开发流程

  1. 建目录:pugins/<平台 id>/{__init__.py, main.py, pugin.json, utils/__init__.py}。

  2. 在 main.py 里按需实现上表槽位;cookie 只保留本平台域名,会话失效抛 platforms.errors.SessionExpired。

  3. 文件末尾注册:

    PLATFORM = registry.register(Platform(
        id="<平台 id>", label="<展示名>", entries=("<登录域名>", "<学习域名>"),
        session=new_session, dump_cookies=dump_cookies, login=login,
        list_courses=list_courses, discover=discover, study=study,
    ))
    
  4. 填 pugin.json(当前只作文档用)。

  5. 接入加载:加载器就绪前,还需要在 platforms/__init__.py 的 load_builtin() 里加一行 import,插件才会被注册。

  6. 联调顺序:GET /api/platforms(下拉里出现该平台)→ /api/login → /api/courses → /api/discover → /api/study。引擎、路由、前端都不需要改。

硬约束

  • 日志只走 runtime.log(key, msg, level=...),key 用引擎传进来的账号 id;不要自己加时间前缀。
  • 循环里要响应停止:(runtime.RUNS.get(key) or {}).get("status") == "stopped" 时返回。
  • 会话失效抛 SessionExpired;自动续期 / 用密码重登由引擎统一处理,插件不要自己重登 (renew_session 里除外)。
  • 不 import engine / routes / 前端;平台之间只允许同平台不同代际互相 import, 且模块级互相 import 成环时改用函数内延迟导入。
  • 时间、解析、编码这类通用能力用现成库,不要手写。
  • 通用能力(契约、注册表、错误类型、日志)放 platforms/,平台私有代码放自己的目录。

联调检查表

  • GET /api/platforms 里能看到该平台,label 正确
  • 添加账号 → 选平台 → 登录成功,卡片显示 profile 名字
  • 刷新课程返回非空列表,in_study 标记正确
  • 发现课程返回的活动数与页面一致
  • 开始学习:日志出现在该账号卡片里,进度递增
  • 点停止后几秒内循环退出
  • 会话失效时能走到续期 / 重登,不需要手动刷新
  • 平台没实现 session 时,/api/courses 明确报「还没有实现 session」

方案演进

  1. 早期:平台实现直接写在 engine.py 里。
  2. 拆包:platforms/base.py 定义 Platform 契约 + registry 注册表,引擎按 course["platform"] 分发;学习流程仍在引擎。
  3. 契约补全:contract.py 用「已实现槽位 → 能力名」推导 capabilities;SessionExpired 移到 platforms/errors.py;各平台的学习流程从引擎搬回平台模块(engine.py 只剩账号级编排)。
  4. 目录化(当前):一个平台一个目录 pugins/<id>/,用 pugin.json 描述元数据, platforms/ 里的实现逐步迁入。

待定项

  • 加载器:扫描 pugins/*/ 自动 import,还是继续在 load_builtin() 手写 import?
  • pugin.json 与 main.py 的字段重复,谁是唯一来源(若以 json 为准,注册就要由加载器做)?
  • 命名:pugins / pugin.json / pugin.md 是否是最终拼写(英文通常是 plugins / plugin.json)。
  • platforms/ 是否最终删除;莞易学(jspx / study)是否迁入 pugins/。
  • platforms/gdgxk.py 与 pugins/gdgxk/main.py 目前是两份副本,迁移完成前以哪份为准。
  • pugin.json 的 "id": "PLATFORM_ID" 应为具体值 "gdgxk"。

相关文档:architecture.md(分层与平台契约)、OPTIMIZATION.md(待实测项)、 progress/todo.md(后续待办)。