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

216 lines
7.4 KiB
JavaScript
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.
#!/usr/bin/env node
/**
* 莞易学(study.dgjy.net)学习链路的 axios 实现。
*
* 只做三件事:查进度 / 课时上报 / 完成后回读复核。
* 不负责登录(Middle-Token 从浏览器 cookie 里复制),也不假装能绕过服务端校验。
*
* 用法:
* MIDDLE_TOKEN='<document.cookie 里 Middle-Token 的值>' \
* node script/reference/study-axios.mjs status 123456
*
* MIDDLE_TOKEN=... node script/reference/study-axios.mjs watch 123456
* MIDDLE_TOKEN=... node script/reference/study-axios.mjs watch 123456 --beat 2 --limit 900
*
* 依赖:axios(npm i axios),Node >= 18。
*/
import axios from 'axios';
const BASE = process.env.DGJY_BASE || 'https://study.dgjy.net/train';
const TOKEN = process.env.MIDDLE_TOKEN || '';
if (!TOKEN) {
console.error('缺少 MIDDLE_TOKEN(浏览器 F12 → Application → Cookies → study.dgjy.net → Middle-Token)');
process.exit(1);
}
const http = axios.create({
baseURL: BASE,
timeout: 20000,
headers: {
'Content-Type': 'application/json;charset=utf-8',
Authorization: `Bearer ${TOKEN}`,
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36',
Referer: 'https://study.dgjy.net/training/',
},
});
// 站点统一返回 {code, msg, data},HTTP 200 也可能是业务失败
http.interceptors.response.use(
(r) => {
const b = r.data;
if (!b || typeof b !== 'object') throw new Error(`${r.config.url} → 非 JSON 响应`);
if (b.code !== 200) throw new Error(`${r.config.url} → code=${b.code} ${b.msg || b.message || ''}`);
return b.data;
},
(e) => {
const u = e.config ? `${e.config.method} ${e.config.url}` : '';
const s = e.response ? `HTTP ${e.response.status}` : e.code || e.message;
throw new Error(`${u} → ${s}`);
},
);
// ---------------------------------------------------------------- 接口
const getTrain = (id) =>
http.get('/course/front/trainCourse/getById', { params: { id } });
const getTree = (courseId) =>
http.get('/course/front/period/list/tree/studyLog', { params: { courseId, isFront: 1 } });
const getPeriod = (periodId) =>
http.get('/course/front/period/getById', { params: { periodId: Number(periodId) } });
// 播放前置校验:{ canPlay, periodId, timeRemaining }
// canPlay=false 表示「你有未完成课时,请按顺序播放」
const getLimit = (periodId) =>
http.get('/course/front/period/studyLimit', { params: { periodId: Number(periodId) } });
// 播放器每 2 秒发一次 isComplete=0,播到结尾发一次 isComplete=1(服务端据此写 completeTime)
const postLog = (periodId, isComplete, lastProgressTime) =>
http.post('/course/front/period/study/log', {
periodId: Number(periodId),
isComplete: isComplete ? 1 : 0,
lastProgressTime: String(Math.round(lastProgressTime || 0)),
});
// ---------------------------------------------------------------- 工具
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const CN = { 1: '视频', 2: '回放', 5: '考试', 6: '作业', 12: '评价', 17: '培训评价' };
function flatten(nodes, out = []) {
for (const n of nodes || []) {
if (n.periodType || n.type === '3') out.push(n);
flatten(n.children || n.periodList || [], out);
}
return out;
}
const isDone = (p) => Boolean(p.studyLog && p.studyLog.completeTime);
const mark = (p) => (isDone(p) ? '✔ 完成' : p.studyLog ? '◐ 学习中' : '○ 未学');
// ---------------------------------------------------------------- 命令
async function status(trainCourseId) {
const t = await getTrain(trainCourseId);
const st = t.study || {};
console.log(
`培训:${t.courseName || t.name || '?'}\n` +
`进度:${st.periodStudy ?? '?'}/${st.periodTotal ?? '?'} 分数:${st.courseStudyScore ?? '-'}`,
);
const tree = await getTree(t.id || trainCourseId);
for (const p of flatten(tree)) {
const type = CN[String(p.periodType)] || `type${p.periodType}`;
console.log(
`${mark(p)} id=${p.id} ${type} ${p.name} fileType=${(p.file && p.file.fileType) || '-'}` +
(p.studyLog && p.studyLog.lastProgressTime ? ` last=${p.studyLog.lastProgressTime}` : ''),
);
}
return t;
}
async function watch(trainCourseId, opts) {
const t = await getTrain(trainCourseId);
const tree = await getTree(t.id || trainCourseId);
const all = flatten(tree);
const pending = all.filter((p) => !isDone(p) && ['1', '2', '3'].includes(String(p.periodType)));
if (!pending.length) {
console.log('没有待学的视频课时');
return;
}
for (const p of pending) {
const label = `[${p.id}] ${p.name}`;
try {
const limit = await getLimit(p.id);
if (limit && limit.canPlay === false) {
console.log(`${label} 服务端要求按顺序播放,先学课时 ${limit.periodId},跳过`);
continue;
}
if (limit && limit.timeRemaining) console.log(`${label} 今日剩余播放时长 ${limit.timeRemaining}`);
} catch (e) {
console.log(`${label} 前置校验不可用(继续尝试):${e.message}`);
}
const fileType = (p.file && p.file.fileType) || '';
if (fileType === 'mooc_video' || fileType === 'cloud_video') {
console.log(`${label} fileType=${fileType}:这种课时的进度由播放器上报,纯接口大概率写不进 completeTime`);
}
let dur = Number((p.file && p.file.duration) || p.duration || 0);
if (!dur) {
try {
const d = await getPeriod(p.id);
dur = Number((d.file && d.file.duration) || d.duration || 0);
} catch {
/* ignore */
}
}
dur = Math.min(dur || 600, Number(opts.limit) || 900);
console.log(`${label} 开始上报,总时长 ${dur}s,心跳 ${opts.beat}s`);
let failed = false;
for (let cur = 0; cur <= dur; cur += opts.beat) {
await sleep(opts.beat * 1000);
try {
await postLog(p.id, 0, cur);
} catch (e) {
console.log(`${label} 心跳失败:${e.message}`);
failed = true;
break;
}
}
if (failed) continue;
try {
await postLog(p.id, 1, dur);
} catch (e) {
console.log(`${label} 完成上报失败:${e.message}`);
continue;
}
await sleep(1000);
try {
const d = await getPeriod(p.id);
const sl = d.studyLog || {};
if (sl.completeTime) console.log(`${label} ✔ 服务端已写 completeTime=${sl.completeTime}`);
else console.log(`${label} ✘ 接口 200 但 completeTime 仍为空(进度 ${sl.progress ?? '?'})`);
} catch (e) {
console.log(`${label} 回读失败:${e.message}`);
}
}
const after = await getTrain(trainCourseId);
const st = after.study || {};
console.log(`最终进度:${st.periodStudy ?? '?'}/${st.periodTotal ?? '?'}`);
}
// ---------------------------------------------------------------- CLI
const [cmd, id, ...rest] = process.argv.slice(2);
const argv = {};
for (let i = 0; i < rest.length; i += 2) {
if (rest[i].startsWith('--')) argv[rest[i].slice(2)] = rest[i + 1];
}
const opts = { beat: Number(argv.beat) || 2, limit: Number(argv.limit) || 900 };
try {
if (cmd === 'status') await status(id);
else if (cmd === 'watch') await watch(id, opts);
else {
console.log('用法:node script/reference/study-axios.mjs <status|watch> <trainCourseId> [--beat 2] [--limit 900]');
process.exit(2);
}
} catch (e) {
console.error('失败:', e.message);
process.exit(1);
}