feat(platforms): 增加能力推导与 /api/platforms capabilities 字段
新增 platforms/contract.py:生命周期 ↔ 槽位 ↔ 能力名映射,capabilities_of 按非空槽位推导;Platform.capabilities() 供 /api/platforms 返回 capabilities。行为不变,现有流程与前端未改。同步 architecture.md(生命周期与能力、广东公需课)、todo 第二/三步、OPTIMIZATION 平台列表。
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
### 做了什么
|
### 做了什么
|
||||||
|
|
||||||
- 「添加账号」弹窗最上面新增「学习平台」下拉,选项来自 `GET /api/platforms`(当前只有「莞易学」)。
|
- 「添加账号」弹窗最上面新增「学习平台」下拉,选项来自 `GET /api/platforms`(当前有「莞易学」「广东公需课」)。
|
||||||
- 账号记录新增 `platform` 字段(SQLite 老库启动时自动 `ALTER TABLE` 补列)。
|
- 账号记录新增 `platform` 字段(SQLite 老库启动时自动 `ALTER TABLE` 补列)。
|
||||||
- 登录(`/api/login`)、刷新课程(`/api/courses`)、发现课程(`/api/discover`、`/api/quiz`)、
|
- 登录(`/api/login`)、刷新课程(`/api/courses`)、发现课程(`/api/discover`、`/api/quiz`)、
|
||||||
开始学习(引擎 `run_one_course`)都按账号所选平台取 `Platform` 上的实现;选「莞易学」时
|
开始学习(引擎 `run_one_course`)都按账号所选平台取 `Platform` 上的实现;选「莞易学」时
|
||||||
|
|||||||
+24
-2
@@ -72,6 +72,7 @@ class Platform:
|
|||||||
discover: Callable # (session, plan_id, project_id, name) -> 课程描述
|
discover: Callable # (session, plan_id, project_id, name) -> 课程描述
|
||||||
renew_session: Callable # (session) -> bool,无密码续期
|
renew_session: Callable # (session) -> bool,无密码续期
|
||||||
profile: Callable # (session) -> 显示名
|
profile: Callable # (session) -> 显示名
|
||||||
|
# 槽位为 None = 该平台没有这项能力;capabilities() 按非空槽位推导
|
||||||
```
|
```
|
||||||
|
|
||||||
`registry.detect(url, html)` 按 `entries` 命中平台;「添加账号」时选择的平台写进
|
`registry.detect(url, html)` 按 `entries` 命中平台;「添加账号」时选择的平台写进
|
||||||
@@ -82,6 +83,21 @@ class Platform:
|
|||||||
`opts` 固定包含:`workers / cap / enable_quiz / enable_feedback / mode / parallel /
|
`opts` 固定包含:`workers / cap / enable_quiz / enable_feedback / mode / parallel /
|
||||||
quiz_key / cookies / plan / project / name`。插件可以只取自己需要的。
|
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`(学习入口)是同一个平台,统一注册为
|
`jspx.dgjy.net`(登录入口)和 `study.dgjy.net`(学习入口)是同一个平台,统一注册为
|
||||||
@@ -94,6 +110,12 @@ quiz_key / cookies / plan / project / name`。插件可以只取自己需要的
|
|||||||
- `platforms/guanyixue.py`:注册平台并按 `course["generation"]` 分发;
|
- `platforms/guanyixue.py`:注册平台并按 `course["generation"]` 分发;
|
||||||
`platforms/detect.py` 只判断页面代际,不判断平台归属。
|
`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`:
|
1. 新建 `src/study_tool/platforms/<名字>.py`:
|
||||||
@@ -120,8 +142,8 @@ quiz_key / cookies / plan / project / name`。插件可以只取自己需要的
|
|||||||
3. 平台注册后会自动出现在「添加账号」的平台下拉框;账号选了该平台,登录 / 刷新课程 /
|
3. 平台注册后会自动出现在「添加账号」的平台下拉框;账号选了该平台,登录 / 刷新课程 /
|
||||||
发现课程 / 学习都走它的实现,不需要改路由或前端。
|
发现课程 / 学习都走它的实现,不需要改路由或前端。
|
||||||
|
|
||||||
现阶段登录与课程发现由莞易学模块承担(SSO 只有一套);`accounts.platform` 已经是平台分发的
|
已内置莞易学(`guanyixue`)和广东公需课(`gdgxk`);`accounts.platform` 是平台分发依据,
|
||||||
依据,接入第二个平台时在 `platforms/<平台>.py` 里实现上面的入口即可。
|
接入新平台只需在 `platforms/<平台>.py` 实现对应阶段的槽位,引擎与路由不用改。
|
||||||
|
|
||||||
## 前端
|
## 前端
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,7 @@
|
|||||||
- 广东公需课:用一门**未学完**的课实测进度上报,确认平台端「已学时长 / 已完成」状态跟着变。
|
- 广东公需课:用一门**未学完**的课实测进度上报,确认平台端「已学时长 / 已完成」状态跟着变。
|
||||||
- 广东公需课:**试卷考核**未接入 —— 平台判定通过要「所有教学活动 100% + 试卷考核 ≥60 分」,所以视频报满后课程仍显示「未完成」。
|
- 广东公需课:**试卷考核**未接入 —— 平台判定通过要「所有教学活动 100% + 试卷考核 ≥60 分」,所以视频报满后课程仍显示「未完成」。
|
||||||
- 广东公需课:视频弹题未接入。
|
- 广东公需课:视频弹题未接入。
|
||||||
|
- 平台契约第二步:`SessionExpired` 下沉到 `platforms/`、通用学习流程从 `engine.py` 搬到平台层公共模块,
|
||||||
|
去掉 `routes/*` 对 `platforms.jspx` 的 star import(新平台漏写 `session` 时会静默按 dgjy 过滤 cookie)。
|
||||||
|
- 平台契约第三步:把答题下沉为平台能力(`quiz`),`quiz.py` 只留 LLM 与通用交卷;前端按 `/api/platforms`
|
||||||
|
返回的 `capabilities` 显示答题 / 反馈按钮。
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ Platform.study 的签名约定:
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from study_tool.platforms.contract import capabilities_of
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Platform:
|
class Platform:
|
||||||
@@ -39,3 +41,7 @@ class Platform:
|
|||||||
"""登录入口或学习入口命中任意片段,就认为页面属于本平台。"""
|
"""登录入口或学习入口命中任意片段,就认为页面属于本平台。"""
|
||||||
text = "%s\n%s" % (url or "", html or "")
|
text = "%s\n%s" % (url or "", html or "")
|
||||||
return any(e and e in text for e in self.entries)
|
return any(e and e in text for e in self.entries)
|
||||||
|
|
||||||
|
def capabilities(self):
|
||||||
|
"""已实现的能力名(见 contract.CAPABILITIES),/api/platforms 用它。"""
|
||||||
|
return capabilities_of(self)
|
||||||
|
|||||||
@@ -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)]
|
||||||
@@ -20,8 +20,10 @@ from study_tool.settings_store import load_settings
|
|||||||
|
|
||||||
@app.get("/api/platforms")
|
@app.get("/api/platforms")
|
||||||
def api_platforms():
|
def api_platforms():
|
||||||
"""已注册平台;前端用 label 展示,例如「莞易学」。"""
|
"""已注册平台;label 给界面展示,capabilities 说明该平台实现了哪些阶段。"""
|
||||||
return {"ok": True, "platforms": [{"id": p.id, "label": p.label} for p in registry.all()]}
|
return {"ok": True, "platforms": [{"id": p.id, "label": p.label,
|
||||||
|
"capabilities": p.capabilities()}
|
||||||
|
for p in registry.all()]}
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ 运行日志 / 快照
|
# ------------------------------------------------------------------ 运行日志 / 快照
|
||||||
|
|||||||
Reference in New Issue
Block a user