新增 platforms/contract.py:生命周期 ↔ 槽位 ↔ 能力名映射,capabilities_of 按非空槽位推导;Platform.capabilities() 供 /api/platforms 返回 capabilities。行为不变,现有流程与前端未改。同步 architecture.md(生命周期与能力、广东公需课)、todo 第二/三步、OPTIMIZATION 平台列表。
8.5 KiB
架构说明
目录
app.py 启动壳:把 src 加进 sys.path,调用 study_tool.app.main()
pyproject.toml 打包配置(src 布局,console script: study-tool)
src/study_tool/
__init__.py 包信息(版本)
__main__.py python -m study_tool 入口
app.py 组装:FastAPI、鉴权中间件、页面 / health、启动 main()
webapp.py FastAPI 实例(路由模块从这里注册,避免循环导入)
config.py 常量 / 环境变量 / 根目录定位(data、static、script)
runtime.py 运行状态 RUNS + 统一日志 log()
store.py SQLite:账号 / 设置 / 日志 / 状态 / 缓存 / 快照
models.py HTTP 请求模型(Pydantic)
settings_store.py 系统设置读写
llm.py 大模型调用(OpenAI 兼容)
quiz.py 测验解析 / 交卷 / AI 答题(含 /api/quiz/*)
engine.py 学习引擎:登录→发现→按平台分发→worker / 视频 / 提交
capture.py 抓包
platforms/ 平台插件(见下)
base.py Platform 契约(id / label / study / entries)
registry.py 注册表:register / get / detect / all
detect.py 页面代际识别(legacy 旧版 / spa 新版)
guanyixue.py 莞易学平台:合并 jspx 登录入口与 study 学习入口
jspx.py 莞易学旧版页面实现(会话 / 登录 / 课程抓取 / 旧版学习)
study.py 莞易学新版 SPA 实现(StudyClient / 接口学习)
routes/ HTTP 接口
accounts.py 账号 / 登录 / 登录记录
misc.py 课程 / 发现 / 学习 / 停止 / 抓包 / 快照 / 平台列表
logs.py /api/log、/api/runs
web/ 前端源码(React 19 + TypeScript + Vite + Tailwind CSS v4)
static/ 前端构建产物(index.html + assets/ + favicon.svg,提交进仓库)
script/ 服务端 JS 脚本:des.js、desenc.js(Node 调用)
script/reference/ 不参与运行的参考脚本:dgjy-toolkit.user.js、study-axios.mjs
分层
- 入口层:
app.py(根)+study_tool/app.py。前者只负责sys.path与启动,后者组装应用。 - 接口层:
routes/*与quiz.py里的/api/quiz/*,只做参数校验、调用引擎、返回结果。 - 领域层:
engine.py(学习流程编排)、platforms/*(平台实现)、llm.py/quiz.py的解析逻辑。 - 基础设施:
store.py(SQLite)、runtime.py(状态 + 日志)、config.py。
依赖方向单向:接口 → 领域 → 基础设施。基础设施不反向 import 接口。
平台插件与识别
平台实现在 platforms/,通过注册表注入,engine 只按 course["platform"] 分发:
# engine.run_one_course 末尾
pf = registry.get(d.get("platform"))
pf.study(key, s, d, workers=..., cap=..., enable_quiz=..., ...)
Platform 契约(见 platforms/base.py):
@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() 按非空槽位推导
registry.detect(url, html) 按 entries 命中平台;「添加账号」时选择的平台写进
accounts.platform,之后登录 / 刷新课程 / 发现课程都用 registry.get(account["platform"])
取实现。课程发现时把平台 id 写进 course["platform"],把页面代际写进
course["generation"](legacy / spa);引擎按平台分发,平台内部再按代际选择学习实现。
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(学习入口)是同一个平台,统一注册为
id=guanyixue、label=莞易学。两代课程页:
platforms/jspx.py:旧版页面(tier-tree-one/tier3),负责会话、登录、课程抓取, 学习流程在engine._study_builtin。platforms/study.py:新版 Vue SPA,StudyClient+/train/course/front/*接口, 学习流程在engine.run_study_course。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。没有测验 / 反馈表,所以不实现对应能力。
新增一个平台
-
新建
src/study_tool/platforms/<名字>.py: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, ...) ... 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>, )) -
在
platforms/__init__.py的load_builtin()里加一行from study_tool.platforms import <名字>。 -
平台注册后会自动出现在「添加账号」的平台下拉框;账号选了该平台,登录 / 刷新课程 / 发现课程 / 学习都走它的实现,不需要改路由或前端。
已内置莞易学(guanyixue)和广东公需课(gdgxk);accounts.platform 是平台分发依据,
接入新平台只需在 platforms/<平台>.py 实现对应阶段的槽位,引擎与路由不用改。
前端
web/ 是独立的前端工程:React 19 + TypeScript + Vite + Tailwind CSS v4。
web/src/App.tsx:账号轮询、操作编排、弹窗状态;components/放卡片 / 弹窗 / 基础 UI。web/src/lib/api.ts:统一 fetch 封装(401 / 离线判定)与 SSE 读取。web/src/types.ts:账号、课程、运行状态、测验等接口类型。- 构建产物输出到仓库根的
static/(index.html+assets/*+favicon.svg), FastAPI 直接托管:/返回static/index.html(禁止缓存),/assets挂StaticFiles。 - 开发:
cd web && pnpm install && pnpm dev(5173,/api代理到127.0.0.1:8848)。 - 构建 / 类型检查:
pnpm build/pnpm typecheck。
日志
统一入口 runtime.log(key, msg, level="info"),一次写三处:控制台、内存
(/api/log 给前端轮询)、SQLite(/api/log/history)。key 用账号 id 或 system。
规范见 AGENTS.md 的「日志规范」,不要自己 print()。