Files
study-tool/README.md
T
study-tool 585a7a28dd feat: study modes (fast/parallel) and course-container completion fix
- settings: study.mode fast|normal and study.parallel 1-5

- normal mode: one headless browser per video, visibility keep-alive, auto-degrade when the platform enforces order

- report study/log/detail/save before isComplete; drop lastProgressTime on completion

- complete periodType 10/13 container nodes and recompute counters via PUT calFinalScore

- capture mode: POST /api/capture records all XHR/fetch including new tabs

- UI: study mode in settings, capture / stop-capture menu items
2026-10-27 22:30:57 +08:00

208 lines
10 KiB
Markdown
Raw 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.
# 多账号学习控制台
东莞市教师培训平台(jspx.dgjy.net / study.dgjy.net)的多账号挂机控制台。账号、会话、课程缓存、运行日志、系统设置全部存在服务端 SQLite,浏览器只是界面,换电脑、远程打开都在。
## 启动
### 方式一:本机(Windows)
```bat
start.bat
```
或手动:
```bat
pip install -r requirements.txt
python app.py
```
浏览器打开 <http://127.0.0.1:8848>(`localhost` 也可以,服务同时监听 `::1`)。
换端口 / 对外开放:
```bat
set PORT=8849 && python app.py
set HOST=0.0.0.0 && python app.py
```
### 方式二:Docker(推荐用于远程管理)
```bash
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`,镜像能小一大截。
## 数据存储(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 服务端:登录链路 / 课程抓取 / 课时上报 / 测验 / 反馈表 / HTTP 接口
store.py SQLite3 存储层:账号 / 设置 / 登录记录 / 运行日志 / 状态 / 缓存 / 快照
static/index.html 前端:账号卡片 + 卡片内「课程日志」折叠面板(无框架,原生 JS)
des.js desenc.js DES 加密(登录表单 rsa 字段,站点原版算法)
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 库(账号/设置/日志/快照)
study_video.py 独立命令行工具:按 trainCourseId 直接刷视频课时(会话也存 study.db)
dgjy-toolkit.user.js 浏览器油猴脚本(独立使用,与网页端无关)
```
## 登录链路
```
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()`。
## 接口
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/` | 控制台页面 |
| GET | `/api/health` | 存活探测:pid / port / runs / SQLite 路径与行数(不需要 token) |
| 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"`:
```bash
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>` 打开一次