diff --git a/docs/OPTIMIZATION.md b/docs/OPTIMIZATION.md index 3ca2021..b78c6a1 100644 --- a/docs/OPTIMIZATION.md +++ b/docs/OPTIMIZATION.md @@ -8,7 +8,7 @@ ### 做了什么 -- 「添加账号」弹窗最上面新增「学习平台」下拉,选项来自 `GET /api/platforms`(当前只有「莞易学」)。 +- 「添加账号」弹窗最上面新增「学习平台」下拉,选项来自 `GET /api/platforms`(当前有「莞易学」「广东公需课」)。 - 账号记录新增 `platform` 字段(SQLite 老库启动时自动 `ALTER TABLE` 补列)。 - 登录(`/api/login`)、刷新课程(`/api/courses`)、发现课程(`/api/discover`、`/api/quiz`)、 开始学习(引擎 `run_one_course`)都按账号所选平台取 `Platform` 上的实现;选「莞易学」时 diff --git a/docs/architecture.md b/docs/architecture.md index a06d363..e69bfe4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -72,6 +72,7 @@ class Platform: discover: Callable # (session, plan_id, project_id, name) -> 课程描述 renew_session: Callable # (session) -> bool,无密码续期 profile: Callable # (session) -> 显示名 + # 槽位为 None = 该平台没有这项能力;capabilities() 按非空槽位推导 ``` `registry.detect(url, html)` 按 `entries` 命中平台;「添加账号」时选择的平台写进 @@ -82,6 +83,21 @@ class Platform: `opts` 固定包含:`workers / cap / enable_quiz / enable_feedback / mode / parallel / quiz_key / cookies / plan / project / name`。插件可以只取自己需要的。 +### 生命周期与能力 + +阶段与槽位对应,缺的阶段不实现即可(不要抛 `NotImplementedError`): + +- 登录 / 会话:`login`、`session`、`renew_session`、`profile` +- 查询课程:`list_courses` +- 课程详情:`discover` +- 学习:`study` +- 测验:暂未进契约(`quiz.py` 仍是莞易学专用),下沉为平台能力后再加 `quiz` + +`Platform.capabilities()` 按非空槽位推导能力名(映射见 `platforms/contract.py`): +`login` / `session` / `renew` / `profile` / `courses` / `discover` / `study`。 +`GET /api/platforms` 一并返回 `capabilities`,前端据此决定显示哪些按钮。 +流程顺序只有一个实现(引擎),插件只负责单步。 + ### 内置:莞易学 `jspx.dgjy.net`(登录入口)和 `study.dgjy.net`(学习入口)是同一个平台,统一注册为 @@ -94,6 +110,12 @@ quiz_key / cookies / plan / project / name`。插件可以只取自己需要的 - `platforms/guanyixue.py`:注册平台并按 `course["generation"]` 分发; `platforms/detect.py` 只判断页面代际,不判断平台归属。 +### 内置:广东公需课 + +`id=gdgxk`,`platforms/gdgxk.py` 单文件实现:在 `jsglpt.gds.edu.cn` 登录(账号、密码 +都是 base64),再用 `ssoItlmsGxkTicket.action` 换票进 `jsxx.gds.edu.cn`;视频按真实 +流逝时间上报 `updateViewTime1`。没有测验 / 反馈表,所以不实现对应能力。 + ### 新增一个平台 1. 新建 `src/study_tool/platforms/<名字>.py`: @@ -120,8 +142,8 @@ quiz_key / cookies / plan / project / name`。插件可以只取自己需要的 3. 平台注册后会自动出现在「添加账号」的平台下拉框;账号选了该平台,登录 / 刷新课程 / 发现课程 / 学习都走它的实现,不需要改路由或前端。 -现阶段登录与课程发现由莞易学模块承担(SSO 只有一套);`accounts.platform` 已经是平台分发的 -依据,接入第二个平台时在 `platforms/<平台>.py` 里实现上面的入口即可。 +已内置莞易学(`guanyixue`)和广东公需课(`gdgxk`);`accounts.platform` 是平台分发依据, +接入新平台只需在 `platforms/<平台>.py` 实现对应阶段的槽位,引擎与路由不用改。 ## 前端 diff --git a/docs/progress/todo.md b/docs/progress/todo.md index 137fa17..4baf34c 100644 --- a/docs/progress/todo.md +++ b/docs/progress/todo.md @@ -8,3 +8,7 @@ - 广东公需课:用一门**未学完**的课实测进度上报,确认平台端「已学时长 / 已完成」状态跟着变。 - 广东公需课:**试卷考核**未接入 —— 平台判定通过要「所有教学活动 100% + 试卷考核 ≥60 分」,所以视频报满后课程仍显示「未完成」。 - 广东公需课:视频弹题未接入。 +- 平台契约第二步:`SessionExpired` 下沉到 `platforms/`、通用学习流程从 `engine.py` 搬到平台层公共模块, + 去掉 `routes/*` 对 `platforms.jspx` 的 star import(新平台漏写 `session` 时会静默按 dgjy 过滤 cookie)。 +- 平台契约第三步:把答题下沉为平台能力(`quiz`),`quiz.py` 只留 LLM 与通用交卷;前端按 `/api/platforms` + 返回的 `capabilities` 显示答题 / 反馈按钮。 diff --git a/src/study_tool/platforms/base.py b/src/study_tool/platforms/base.py index ba3ed8c..e9f112d 100644 --- a/src/study_tool/platforms/base.py +++ b/src/study_tool/platforms/base.py @@ -19,6 +19,8 @@ Platform.study 的签名约定: from dataclasses import dataclass from typing import Callable, Optional +from study_tool.platforms.contract import capabilities_of + @dataclass class Platform: @@ -39,3 +41,7 @@ class Platform: """登录入口或学习入口命中任意片段,就认为页面属于本平台。""" text = "%s\n%s" % (url or "", html or "") return any(e and e in text for e in self.entries) + + def capabilities(self): + """已实现的能力名(见 contract.CAPABILITIES),/api/platforms 用它。""" + return capabilities_of(self) diff --git a/src/study_tool/platforms/contract.py b/src/study_tool/platforms/contract.py new file mode 100644 index 0000000..5c39419 --- /dev/null +++ b/src/study_tool/platforms/contract.py @@ -0,0 +1,29 @@ +"""平台插件契约:生命周期、能力与槽位对应关系。 + +插件只实现自己平台**确实有**的阶段,缺的阶段不要实现,也不要抛 NotImplementedError; +引擎按能力判断是否走该分支(例如没有 `login` 的平台不显示密码登录)。 + + 生命周期 槽位 能力名 + 登录 / 会话 login / session / renew_session / profile login / session / renew / profile + 查询课程 list_courses courses + 课程详情 discover discover + 学习 study study + 测验 (待 quiz 下沉为平台能力后加入) quiz + +流程(按什么顺序调用)只有一个实现,在引擎里;平台只负责「这一步在这个站点怎么做」。 +""" +CAPABILITIES = { + "login": ("login",), + "session": ("session",), + "renew": ("renew_session",), + "profile": ("profile",), + "courses": ("list_courses",), + "discover": ("discover",), + "study": ("study",), +} + + +def capabilities_of(platform) -> list: + """按已实现的槽位推导能力名,顺序固定(即 CAPABILITIES 的顺序)。""" + return [name for name, slots in CAPABILITIES.items() + if all(getattr(platform, slot, None) for slot in slots)] diff --git a/src/study_tool/routes/misc.py b/src/study_tool/routes/misc.py index ee25c66..ba8620c 100644 --- a/src/study_tool/routes/misc.py +++ b/src/study_tool/routes/misc.py @@ -20,8 +20,10 @@ from study_tool.settings_store import load_settings @app.get("/api/platforms") def api_platforms(): - """已注册平台;前端用 label 展示,例如「莞易学」。""" - return {"ok": True, "platforms": [{"id": p.id, "label": p.label} for p in registry.all()]} + """已注册平台;label 给界面展示,capabilities 说明该平台实现了哪些阶段。""" + return {"ok": True, "platforms": [{"id": p.id, "label": p.label, + "capabilities": p.capabilities()} + for p in registry.all()]} # ------------------------------------------------------------------ 运行日志 / 快照