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

122 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 更新日志
## 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 个),现在每门课跑完自动重算