# 更新日志 ## 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`:考核是**独立活动页**(`//study/course/`),从课程目录找标题含「考核 / 考试」的活动再解析 `#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 个),现在每门课跑完自动重算