Files
study-tool aa48145557 feat: 故障推送 / 公需课自动重学 / 插件解耦,发版 v0.0.8
体检修复:启动日志不再打印 AUTH_TOKEN、密钥可清空、推送失败不占整段冷却、目录名与文案统一成 plugins/。
2026-11-01 17:47:22 +08:00

16 KiB
Raw Permalink Blame History

更新日志

Unreleased

v0.0.8

体检修复

  • 启动日志不再打印 AUTH_TOKEN 明文:日志同时进控制台 / 内存 / SQLite,前端「系统日志」里能直接看到,之前等于把令牌贴在墙上;现在只提示「需要访问令牌:见 .env 的 AUTH_TOKEN」
  • 系统设置:温度留空(0)不再把已保存的温度清成 0;API Key / app_secret 设置后原本无法清空,字段右侧加「清空」按钮(clear_api_key / notify.clear_app_secret)
  • 故障推送:推送失败不再占用整段 cooldown_min 冷却,留 1 分钟重试,避免一次网络抖动把通知压掉十分钟
  • 用户可见报错里的旧目录名 platforms/ 改成 plugins/
  • pyproject.toml 的版本与 metadata.yaml 对齐,bumpversion 改成同时更新两处,发版不再漂移
  • 文档里的旧目录名 pugins/ 全部改成 plugins/,docs/pugin.md 更名为 docs/plugin.md;.gitignore 补上 .idea/、.ruff_cache/

故障推送

  • 新增通知中心 study_tool/notify.py:fault(kind, key, title, content) 上报故障、retry(key, what) 累计重试、send() 直接推送;同一 (类型, 账号) 按 cooldown_min 冷却,重试只在刚达到阈值时推一次
  • 新增推送渠道插件 plugins/daidai_push/(呆呆面板):/api/open-api/token 换 token(内存缓存、按 expires_in 过期、401 自动换新重发)+ /api/notifications/send;plugin.json 用 "kind": "notify" 声明类型
  • 插件加载器支持非平台插件:kind=notify 的插件注册到 notify 注册表,停用 / 重载同样摘除;/api/plugins 返回 kind,前端能力显示「推送」
  • 设置项新增 notify(base_url / app_key / app_secret / channel_name / 各类型开关 / retry_threshold / cooldown_min),/api/settings 读写(密钥只回显前后几位),新增 POST /api/notify/test 测试推送
  • 触发点:课程学完仍未完成 / 进度没推进 / 没有可识别活动(notify.check_progress)、课程处理异常、整体运行异常、登录失败、会话失效且无法续期;公需课掉线重登与自动重学、莞易学单课时重报计入重试次数
  • 前端「系统设置」改成左侧分类(大模型 / 学习模式 / 故障推送)+ 右侧内容的两栏布局:Modal 新增 settings 宽度档与 bodyClassName 覆盖;「测试连接」「测试推送」分别放进对应面板右上角,弹窗底部只留取消 / 保存

刷新课程 / 会话

  • 修复「平台「」还没有实现刷新课程」:session_expired 自动重登后的重试请求补上 platform,不再把登录成功的结果丢掉
  • 修复反复重登:运行状态里的旧 cookie 不再在运行结束后回写账号(只在 discovering / studying 时同步),重新登录时也会清掉运行状态里的 cookies
  • 公需课学习中掉线重登后,把新会话写回账号与运行状态(新增 database.account_save_cookies),库里不再留失效会话

公需课交互式测验

  • 平台契约新增 quiz_submit(self, s, b) 槽位,能力 quiz 改为「quiz + quiz_submit 都实现」才算;/api/quiz/submit 改成按 registry.get(platform).quiz_submit 分发,莞易学的交卷逻辑移到 quiz.submit_quiz
  • 公需课实现 quiz / quiz_submit:考核是独立活动页(/<aid>/study/course/<cid>),从课程目录找标题含「考核 / 考试」的活动再解析 #testForm(题型映射 01 单选 / 02 多选 / 03 判断),弹窗里看题、AI 答题、交卷;交卷按用户选的字母换算成平台选项值提交,已交卷的课也能回看题目
  • 修复公需课自动考核:exam.run 之前读课程主页(没有 testForm,永远拿不到题),改为读考核活动页
  • 前端测验弹窗交卷请求带上 platform

公需课自动重学

  • 公需课 study 改为「刷一轮 → 重取进度页(/study/course/progess)」循环:即使进入时所有任务点都显示已完成,也会重新读取一次平台状态;仍有未完成任务点时等 60 秒后自动重新开始学习,不再需要手动点「开始学习」
  • 连续 3 轮平台完成数都没增加(如考核等非视频任务点)或达到 5 轮(MAX_ROUNDS)时停止,并在日志里列出剩余任务点

公需课选课兜底

  • 公需课刷新课程时,若 /uc/store/courseRegister 里没有目标年度的课,自动去 /uc/store/course/ 选课:已有「进入学习」的课就直接用,否则按配置选第几门;目标年度和序号放在插件目录 plugins/gdgxk/config.json(year 空 = 当前年份,pick 默认 1),不进数据库、不动前端

插件解耦

  • 插件可直接删除:核心不再 import 或写死任何具体平台。加载器记录插件 import 期间注册的路由,停用 / 重载 / 加载失败时一并摘除(修复重载后 /api/quiz* 重复注册、停用后仍可访问)
  • 平台契约新增能力 quiz、capture;前端「打开测验」「抓包」改为按 GET /api/platforms 的 capabilities 显示;/api/quiz 改走平台的 quiz 能力
  • 去掉写死的 guanyixue 默认值(accounts.platform、请求模型、抓包兜底、前端初值与回退文案),页脚与添加账号提示不再写死平台名
  • 插件专属脚本迁到插件目录:des.js / desenc.js / study_video.py → plugins/guanyixue/script/,capture-jsglpt.py / test-gdgxk.py → plugins/gdgxk/script/;test-gdgxk.py 改用新的插件 API

工程

  • 新增 .pre-commit-config.yaml:提交前跑 ruff 检查 / 格式化、yaml 与私钥 / 大文件检查,web/ 有改动时跑 pnpm typecheck,提交信息由 commitizen 校验(约定式提交);pyproject.toml 增加 [tool.ruff] 与 [tool.bumpversion],发版用 bump-my-version bump patch 同步 metadata.yaml、提交并打 v 开头的 tag

重构

  • 清理 study_tool 顶层的插件专属代码:莞易学的站点地址 / 登录授权入口 / 课程板块 / NODE / DESENC / HEX32 / CODE_RE / DEBUG_DIR 等常量从 config/__init__.py 迁到 plugins/guanyixue/config.py,config/ 只剩 ROOT / DATA / STATIC_DIR 通用路径;测验请求模型(QuizIn / QuizSubmitIn / QuizAnswerIn)、答题提示词 DEFAULT_ANSWER_PROMPT、题型名 TYPE_LABEL 从 models.py / config/settings.py 迁到 plugins/guanyixue/quiz.py;引擎里"新版课程 ID"日志挪进莞易学 study() 分发
  • 删除无人引用的代码:config 里的 LOGDIR(及启动时建 data/logs 目录)、SessionExpired 再导出、models.py 的 AccountsIn
  • app.py / engine.py / routes/accounts.py / routes/misc.py 以及莞易学 utils/* 的 from study_tool.config import *、from study_tool.models import * 改成显式 import
  • 前端从单文件 static/index.html(原生 JS)改为 web/ 工程:React 19 + TypeScript + Vite + Tailwind CSS v4;Vite 直接输出到 static/,FastAPI 托管 / 与 /assets,运行 Python 不再需要 Node
  • 新旧学习平台合并为「莞易学」:jspx.dgjy.net(登录入口)与 study.dgjy.net(学习入口)统一注册为 Platform(id="guanyixue", label="莞易学"),课程页代际(legacy / spa)由 platforms/detect.py 判断,平台内部按代际分发到 jspx.py / study.py
  • Platform 新增 entries(登录 / 学习入口 URL 片段)与 registry.detect();课程发现按入口识别平台,新增平台不再需要改引擎。新增 GET /api/platforms 供前端展示平台名
  • 添加账号新增「学习平台」下拉(数据来自 GET /api/platforms);accounts 表新增 platform 字段(老库启动时自动补列),账号选定平台后,登录 / 刷新课程 / 发现课程分别走该平台的 login / list_courses / discover,学习与测验按 course["platform"] + generation 分发
  • Platform 增加账号级可选入口:session / dump_cookies / login / list_courses / discover / renew_session / profile;引擎和路由按 registry.get(account["platform"]) 取实现,接入第二个平台只需写一个模块
  • 测验交卷从 platform(jspx / study)改为 generation(legacy / spa),接口返回 platform="guanyixue" + generation
  • 代码改为标准 src/ 包布局:业务代码全部在 src/study_tool/,根目录 app.py 只做启动壳(python app.py、start.bat、Dockerfile 都不用改);新增 pyproject.toml,pip install -e . 后可用 study-tool 命令
  • 平台拆成插件包 src/study_tool/platforms/:base.py 定义 Platform 契约、registry.py 注册表;引擎按 course["platform"] 从注册表分发
  • HTTP 接口收进 routes/(accounts / misc / logs),docs/architecture.md 说明分层、平台识别与新增平台的步骤
  • 散落的 JS 脚本统一收进 script/:运行必需的 des.js / desenc.js 放 script/(config.py 的 DESENC 指向 script/desenc.js),不参与运行的 dgjy-toolkit.user.js、study-axios.mjs 移到 script/reference/ 作参考
  • 平台契约第一步:新增 platforms/contract.py 定义生命周期 ↔ 槽位 ↔ 能力名映射,Platform.capabilities() 按已实现槽位推导,GET /api/platforms 返回 capabilities
  • 平台契约第二步:SessionExpired 移到 platforms/errors.py;旧版 / 新版学习流程分别从 engine.py 搬到 platforms/jspx.py / platforms/study.py,engine.py 只剩账号级编排(115 行);routes/*、quiz.py 去掉平台 star import,session / dump_cookies 缺失时明确报错

新增

  • 公开查询页 /query:不需要 AUTH_TOKEN,顶部输入框填账号用户名后点「查询」,显示该账号的课程列表与当前学习进度;对应公开接口 GET /api/query?username=,只返回课程名 / 章节 / 状态 / 进度与运行进度,不含密码、cookie、账号 id
  • 新增平台「广东公需课」(platforms/gdgxk.py):jsglpt.gds.edu.cn 账号密码(base64)登录后换 ITLMS 票进 jsxx.gds.edu.cn,课程列表读 /uc/store/courseRegister;学习只跑 updateViewTime1 进度心跳(服务端按 lastViewTime 直接记账,不需要真播)。票按 ITLMS-xxx||ticketId 两段交给 jsxx.gds.edu.cn/nts/loginToStoreByAccount;登录要过 4 位图形验证码,用 ddddocr 离线识别并自动重试。配套 script/capture-jsglpt.py(抓包取证)与 script/test-gdgxk.py(接口自测);课程列表解析出培训年度 / 学时 / 完成状态,卡片显示成「2026年公需课 · 未完成 · 30 学时」
  • 统一日志源 runtime.log():控制台、内存(/api/log)、SQLite(/api/log/history)同一份内容;新增右上角「系统日志」弹窗查看 key=system 的启动/设置类日志
  • 设置保存、服务启动都写入统一日志;卡片「课程日志」和「系统日志」的「清空」会同时清内存和 SQLite
  • 前端:卡片状态徽标、学习进度、课程勾选、卡片内实时日志、测验 / 设置 / 系统日志弹窗全部保留;新增平台名徽标与空态引导

修复

  • 账号接口的错误分支引用未导入的 JSONResponse、删除账号引用未导入的 RUNS / RUNS_LOCK,登录失败 / 删除账号会 500:补齐导入
  • discover() 引用未定义的 detect_platform / has_study_marker 导致课程发现 NameError:代际识别集中到 platforms/detect.py,两代页面共用同一套判断
  • 系统设置里的大模型配置保存后重新打开会回到默认值:save_settings 写的是嵌套结构 {llm, study},load_settings 却按顶层字段读,现在改成从 llm 子对象读取
  • AI 答题整段同步等待导致浏览器 / 反代超时:/api/quiz/answer 改为流式(text/event-stream)分批返回,每批失败自动重试一次
  • AI 答题解析不出 JSON 时直接判空:补 max_tokens、空内容 / 非 JSON 抛错重试,并支持从自由文本、截断 JSON 里抠答案,多选连写(AC)也能识别
  • 控制台是 GBK 时日志里含 ⚠ 之类字符会让工作线程抛 UnicodeEncodeError:runtime.log 现在编码失败会自动降级
  • 服务重启把上次没跑完的运行标成「已中断」后,那句话会被后续 set_state 一起写回库,新一轮运行一起步就顶着「服务重启,上次运行已中断」,库里还留着 status=studying 的幽灵记录:runtime.set_state() 在状态切入 discovering / studying 时清掉上一轮的 error / progress / finished_at,hydrate_runs() 标记后立即落库
  • 服务重启后卡片「课程日志」显示「暂无日志」,SQLite 里的历史读不出来:前端兜底条件原为「内存为空且 status 为空」,而重启后 status 恰好是 interrupted(非空),历史永远不读;现在只看内存是否为空
  • 删除账号只清了按账号 id 记的 run_state / run_log,登录日志(key 与 login_log.username 记的是用户名 / 手机号)残留:account_delete() 改为按账号 id + 用户名一起清,DELETE /api/accounts/{id} 同时清掉内存里两个 key 的日志
  • 学习链路上三处引用未定义的全局名(platforms/jspx.py 的 extract_study_token / StudyClient / save_snapshot 等、platforms/study.py 的 parse_qs、quiz.py 的 BeautifulSoup / urlsplit)会让课程发现 / 答题直接 NameError:parse_qs、BeautifulSoup、urlsplit 改为模块顶部导入,jspx ↔ study 之间改用函数内延迟导入(模块级会成环)
  • 服务重启留下的「已中断」状态在重新登录后仍然压在卡片上,徽标显示「已中断」并挂着旧进度和红条:登录成功后清掉该账号上一轮已结束的运行状态(内存 + SQLite),正在跑(discovering / studying)的不动,内存日志保留
  • 本机启动(python app.py / uv run app.py)时根目录 .env 完全无效:只有 docker compose 会读它,AUTH_TOKEN / PORT / RUN_LOG_KEEP / SNAPSHOT_KEEP 都得靠命令行环境变量;现在 study_tool/__init__.py 用 python-dotenv 加载 .env(已存在的环境变量优先,Docker 由 compose 注入的不受影响),新增依赖 python-dotenv>=1.0
  • .env 里写 BIND_ADDR 在本机启动时不起作用(代码只读 HOST):监听地址改为 HOST → BIND_ADDR → 127.0.0.1 依次回退

其他

  • 新增 uv.lock 依赖锁定与 uv 安装方式;pyproject.toml 增加阿里云 PyPI 镜像([[tool.uv.index]]),Dockerfile 增加 PIP_INDEX_URL 构建参数(默认阿里云,可回官方源),README 补充 uv / pip / 换源说明
  • .gitignore 忽略 *.egg-info/(uv run 可编辑安装的产物)
  • 文档整理:OPTIMIZATION.md、todo.md 移入 docs/;移除不再使用的 start.bat

v0.0.1

首个版本。

新增

  • 学习方式配置:快速学习(只调 period/study/log 接口)/ 常规学习(开浏览器真播视频,1–5 路并发)
  • 常规学习:每路独立无头浏览器,可见性补丁防暂停;平台按课节顺序放行时自动降为 1 路,并把被指定的课时提到队首
  • 抓包模式:POST /api/capture 记录所有 XHR/fetch(不限域名),window.open 新开的标签页同样挂监听,结果落 data/capture-*.json
  • 界面:系统设置新增「学习方式」(模式 + 并发数),卡片「更多」菜单新增「抓包 / 停止抓包」

修复

  • 视频完成上报补齐学时台账 study/log/detail/save(stime/etime),完成包不再带 lastProgressTime
  • 培训里的「课程」容器节点(periodType 10/13)不会自动完成:补一发 isComplete,并用 PUT /course/front/calFinalScore 重算课程完成计数
  • 并发上报导致课程完成计数丢失(实测 14 个课时只记 11 个),现在每门课跑完自动重算