Files
study-tool/README.md
T
study-tool 1a3347f7f9 feat: 添加账号支持选择学习平台
账号新增 platform 字段,登录/刷新课程/发现课程按平台分发;同时把 JS 脚本收进 script/(目录规范调整)。
2026-10-30 01:13:47 +08:00

14 KiB
Raw Blame History

学习控制台

莞易学(jspx.dgjy.net / study.dgjy.net,新旧版课程页自动识别)的多账号挂机控制台。账号、会话、课程缓存、运行日志、系统设置全部存在服务端 SQLite,浏览器只是界面,换电脑、远程打开都在。

启动

方式一:本机(Windows)

start.bat

或手动:

pip install -r requirements.txt
python app.py

浏览器打开 http://127.0.0.1:8848(localhost 也可以,服务同时监听 ::1)。

换端口 / 对外开放:

set PORT=8849 && python app.py
set HOST=0.0.0.0 && python app.py

方式二:Docker(推荐用于远程管理)

cp .env.example .env
# 编辑 .env:至少设一个 AUTH_TOKEN(远程管理必须有,否则谁都能读到里面的账号密码)
#   AUTH_TOKEN=$(python -c "import secrets;print(secrets.token_urlsafe(24))")

docker compose up -d --build
docker compose logs -f          # 看启动日志

打开 http://<服务器IP>:8848/?token=<AUTH_TOKEN>,令牌会写进 cookie,之后正常用。

操作 命令
起 / 重建 docker compose up -d --build
停 docker compose down
看日志 docker compose logs -f --tail=200
备份数据 cp data/study.db data/study.db.bak(服务运行中也可拷,WAL 模式)
进容器 docker compose exec study-tool bash
换端口 .env 里改 PORT,再 docker compose up -d

环境变量:

变量 默认 说明
HOST 127.0.0.1 监听地址;Docker 里是 0.0.0.0
PORT 8848 端口
DB_PATH <项目>/data/study.db SQLite 库文件
AUTH_TOKEN 空 非空 = 页面/接口需要 ?token= 或 cookie;/api/health 永远放行
RUN_LOG_KEEP 2000 每个账号保留的日志行数
SNAPSHOT_KEEP 200 课程页快照保留条数
NODE_BIN 自动找 node.exe 路径(算登录表单的 DES)

镜像默认装 Chromium(直链视频要真播到结尾)。不需要就用 docker compose build --build-arg WITH_PLAYWRIGHT=0,镜像能小一大截。

前端开发

页面在 web/,技术栈 React 19 + TypeScript + Vite + Tailwind CSS v4。构建产物已提交到 static/,只跑 Python 时不需要装 Node:

cd web
pnpm install
pnpm dev       # http://127.0.0.1:5173,/api 代理到 8848
pnpm build     # 输出到 ../static/,Python 端直接托管
pnpm typecheck

改完前端执行一次 pnpm build,刷新页面即可;static/index.html 禁止缓存。

数据存储(SQLite3)

单文件 data/study.db(WAL 模式,读写不互斥)。表:

表 内容
accounts 账号 / 密码 / 会话 cookie / 课程缓存 / 勾选 / 自动交测验·反馈表开关
settings 大模型配置(base_url / api_key / model / 温度 / 超时 / 提示词)
kv 通用缓存(例如 study 平台试出来的接口前缀、study_video.py 的会话)
login_log 每次登录尝试(平台原话,/api/loginlog 读它)
run_log 每个账号的运行日志(/api/log/history?key=<id>&tail=200)
run_state 运行状态快照,进程重启后 /api/runs 仍能看到,未跑完的标 interrupted
snapshots 站点改版时存下的课程页原始 HTML(/api/snapshots、/api/snapshot/<id>)

旧的 data/settings.json、data/logs/*.log、data/study_session.json 在首次启动时自动导入, 原文件改名为 *.imported(回滚旧版本时改名回去即可)。

密码是明文存在库里的——会话失效时后台要能自己重登。所以这个库等价于账号密码本: 不要提交进 git,Docker 部署时务必设 AUTH_TOKEN,公网前面再加一层 HTTPS 反代。

用法

  1. 右上角「添加账号」→ 选「学习平台」(当前只有莞易学)→ 填账号密码(存进服务端 SQLite)。
  2. 点「登录」→ 自动读 /admin/main.action 的「在学课程」。
  3. 勾选课程 → 「开始学习」→ 卡片底部「课程日志」展开看实时进度。
  4. 「自动交反馈表」默认开;「自动交测验」默认关(答题正确率无法保证)。

测验

卡片上「测验」→ 弹窗按表格列出题号 / 题型 / 题干 / 选项 →「AI 答题」按系统设置里的模型填答案 → 核对后「提交」。

  • 老平台(jspx):showTestQums.action 取题,endTest.action 交卷,回分从 doTest.action 抓
  • 新平台(study.dgjy.net):
    • 取题 GET /train/questions/front/testpaper/getById?testpaperResultId=…
    • 还没生成答卷时先自动创建(不用再手工去页面点一次「开始考试」): PUT /train/questions/front/testpaper/start/do {"periodId": "<课时 id>", "trainCourseId": "<trainCourseId>"} → data.testpaperResultId
    • 交卷 PUT /train/questions/front/result/submitExam {"resultId": "<resultId>", "resultItemList": [{"fileIds": [], "itemId": "<itemResultId>", "replay": [0,2] | null}]}
    • replay 是0 基选项下标数组:历史答案会自动回填,提交时字母答案按选项顺序换成下标
    • 交卷后看答案 GET /train/questions/front/testpaper/teacher/review?testpaperResultId=… 返回 data.questions[].data[] 里的 answer(标准答案)、replay(学生答案)、 answerScore、analysis。只有测验配置 examAfterWatchAnswer=1 时才在页面展示
    • 重考:课程树里的 allowExamCount / surplusExamCount 是总次数 / 剩余次数 (平台一般 3 次)。surplusExamCount > 0 时再调一次 start/do 会生成新答卷, 不需要老师重置;次数用完平台会拒绝
    • 判断题(JUDGMENT)接口里没有选项(metasData 只有一个 null 占位),页面会补出 A 正确 / B 错误,交卷时映射成 [0] / [1];模型写「正确/对/√」也会收敛成 A、B
    • 题组结构 data.questions[].data[],鉴权用 Authorization: Bearer <Middle-Token>

已交卷的测验会显示「已完成 · 得分 X · 及格 Y · 已通过」并禁用「AI 答题」「提交」, 按钮文字变成「已交卷」、旁边写「已交卷 · 不可重复提交」——不会出现点了没反应的情况。 如果后端发现平台已经收卷,会直接提示「已经交过卷了,不能重复提交」,不会偷偷覆盖原成绩。

系统设置

右上角「设置」→ 大模型:base_url(OpenAI 兼容,写到 /v1 为止)、api_key、model、 温度、超时、答题提示词。配置存 SQLite 的 settings 表,api_key 不回显,留空表示不修改。 「测试连接」发一条 8 token 的请求验证连通性。

目录

app.py                     启动壳:把 src 加进 sys.path,调 study_tool.app.main()
pyproject.toml             打包(src 布局;pip install -e . 后可用 study-tool 命令)
src/study_tool/
  app.py                   组装:FastAPI、鉴权中间件、页面 / health、启动 main()
  webapp.py                FastAPI 实例(路由模块从这里注册,避免循环导入)
  runtime.py               运行状态 + 统一日志源(控制台 / 内存 / SQLite)
  config.py                常量、环境变量、根目录定位(data / static / script)
  store.py                 SQLite3:账号 / 设置 / 登录记录 / 运行日志 / 状态 / 缓存 / 快照
  models.py                HTTP 请求模型
  settings_store.py        系统设置读写
  llm.py                   大模型调用
  quiz.py                  测验解析 / 交卷 / AI 答题
  engine.py                学习引擎:发现 → 按平台分发 → worker / 视频 / 提交
  capture.py               抓包
  platforms/               平台插件
    base.py                Platform 契约(id / label / study / entries)
    registry.py            注册表:register / get / detect / all
    detect.py              页面代际识别(旧版 / 新版 SPA)
    guanyixue.py           莞易学:合并 jspx 登录入口与 study 学习入口
    jspx.py                莞易学旧版页面实现
    study.py               莞易学新版 SPA 实现
  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/des.js              服务端 DES 加密(登录表单 rsa 字段,站点原版算法)
script/desenc.js           DES 命令行封装(Node 调用,config.py 里定位)
script/reference/          不参与运行的参考脚本:油猴 dgjy-toolkit.user.js、study-axios.mjs
start.bat                  一键启动(Windows)
Dockerfile                 Docker 镜像(python:3.12-slim + nodejs + chromium)
docker-compose.yml         Docker 编排:端口、数据卷、AUTH_TOKEN、健康检查
.env.example               远程管理用的环境变量样例(复制成 .env)
requirements.txt           依赖
data/study.db              SQLite 库(账号/设置/日志/快照)
docs/architecture.md       分层说明 + 平台插件怎么加
study_video.py             独立命令行工具:按 trainCourseId 直接刷视频课时(会话也存 study.db)

分层与插件契约见 docs/architecture.md:引擎只按 course["platform"] 从注册表取平台实现;平台用 entries 声明登录 / 学习入口,并可按需实现 login / list_courses / discover 等账号级入口;「添加账号」选的平台存进账号记录,登录与课程发现按它分发,新增平台不用改 engine / 路由 / 前端。

日志统一从 runtime.log() 出:控制台、页面卡片「课程日志」(按账号 id)、右上角「系统日志」(key=system) 看到的是同一份;新增日志不要再自己 print()。

登录链路

GET  tyyh.dgjy.net/tpass/login                    取 lt / execution
POST tyyh.dgjy.net/tpass/login                    un pd ul pl rsa(des) lt
  → CASTGC
GET  dj.dgjy.net/.../authorize                    302 + JS 中转页
GET  passport.dgjy.net/scriptLogin                JS 返回 verifyURL
GET  dj.dgjy.net/fpf/login/verify.action          302
GET  jspx.dgjy.net/tstudy/passport/login.action   下发 jspx JSESSIONID
GET  jspx.dgjy.net/admin/main.action              table-results 课程表

requests 不执行 JS,中间两跳是手工解析 scriptLogin.src 和 verifyURL 走完的。

新版页面(慕课)另走一条 OAuth 码换 token 的链,拿到 study.dgjy.net 的 Middle-Token,见 ensure_study_session()。 jspx.dgjy.net 与 study.dgjy.net 在注册表里是同一个平台(guanyixue / 莞易学), 课程发现时自动判断页面代际(旧版 / 新版),分别走 jspx.py / study.py 的学习实现。

接口

方法 路径 说明
GET / 控制台页面
GET /api/health 存活探测:pid / port / runs / SQLite 路径与行数(不需要 token)
GET /api/platforms 已注册平台(id / label),用于界面展示平台名
GET /api/accounts 账号列表(会话、课程缓存、勾选,不含密码)
POST /api/accounts 新增/更新账号;password 不传 = 保留库里原密码
GET /api/accounts/{id} 单个账号
DELETE /api/accounts/{id} 删除账号(连带清掉它的运行状态与日志)
POST /api/login {account_id}(密码从库里取)或 {username,password} → cookies + 课程列表
POST /api/courses 用已有 cookies 刷新课程
POST /api/discover 按 planId/projectId 查课程详情
POST /api/study 开始挂机
POST /api/stop 停止
POST /api/quiz 取一次测验的题目与选项
POST /api/quiz/answer 把题目交给大模型,返回 {题号: 答案}
POST /api/quiz/submit 按题号合并答案后交卷
GET/POST /api/settings 读取 / 保存大模型配置(key 脱敏)
POST /api/llm/test 测试模型连通性
GET /api/log 增量日志 ?key=&since=(内存里的实时日志)
GET /api/log/history SQLite 里的历史日志 ?key=&tail=;不带 key 列出各账号行数
DELETE /api/log/history 清掉某个账号的历史日志 ?key=
GET /api/runs 所有账号运行状态(重启后从 SQLite 恢复)
GET /api/loginlog 登录尝试记录 ?tail=
GET /api/snapshots 课程页快照列表
GET /api/snapshot/{id} 取回某份快照的原始 HTML

远程调用时(设了 AUTH_TOKEN)带上 -H "X-Auth-Token: $TOKEN":

curl -H "X-Auth-Token: $TOKEN" http://host:8848/api/health
curl -X POST -H "X-Auth-Token: $TOKEN" -H "Content-Type: application/json" \
     -d '{"account_id":"a1"}' http://host:8848/api/login

排查

登录失败先看 data/study.db 的 login_log 表(或 GET /api/loginlog)最后一行:

  • 您还剩N次机会… — 平台判密码错,跟链路无关
  • SSO 校验未通过 / jspx 没有建立会话 — 授权链断了,站点改版
  • Incoming markup is of an invalid type — 代码把布尔当 HTML 用了
  • 页面能开、接口全 ERR_CONNECTION_REFUSED — 服务没起或端口被占,netstat -ano | findstr :8848
  • 容器里起来了但连不上 — docker compose logs 看是否 Address already in use,.env 换 PORT
  • 页面 401 / 空白提示需要访问令牌 — 用 /?token=<AUTH_TOKEN> 打开一次