exams.js
1 import { cli, Strategy } from '@jackwener/opencli/registry'; 2 import { getCourses, initSession, enterCourse, getTabIframeUrl, parseExamsFromDom, sleep, } from './utils.js'; 3 cli({ 4 site: 'chaoxing', 5 name: 'exams', 6 description: '学习通考试列表', 7 domain: 'mooc2-ans.chaoxing.com', 8 strategy: Strategy.COOKIE, 9 timeoutSeconds: 90, 10 args: [ 11 { name: 'course', type: 'string', help: '按课程名过滤(模糊匹配)' }, 12 { 13 name: 'status', 14 type: 'string', 15 default: 'all', 16 choices: ['all', 'upcoming', 'ongoing', 'finished'], 17 help: '按状态过滤', 18 }, 19 { name: 'limit', type: 'int', default: 20, help: '最大返回数量' }, 20 ], 21 columns: ['rank', 'course', 'title', 'start', 'end', 'status', 'score'], 22 func: async (page, kwargs) => { 23 const { course: courseFilter, status: statusFilter = 'all', limit = 20 } = kwargs; 24 // 1. Establish session 25 await initSession(page); 26 // 2. Get courses 27 const courses = await getCourses(page); 28 if (!courses.length) 29 throw new Error('未获取到课程列表,请确认已登录学习通'); 30 const filtered = courseFilter 31 ? courses.filter(c => c.title.includes(courseFilter)) 32 : courses; 33 if (courseFilter && !filtered.length) { 34 throw new Error(`未找到匹配「${courseFilter}」的课程`); 35 } 36 // 3. Per-course: enter → click 考试 tab → navigate to iframe → parse 37 const allRows = []; 38 for (const c of filtered) { 39 try { 40 await enterCourse(page, c); 41 const iframeUrl = await getTabIframeUrl(page, '考试'); 42 if (!iframeUrl) 43 continue; 44 await page.goto(iframeUrl); 45 await page.wait(2); 46 const rows = await parseExamsFromDom(page, c.title); 47 allRows.push(...rows); 48 } 49 catch { 50 // Single course failure: skip, continue 51 } 52 if (filtered.length > 1) 53 await sleep(600); 54 } 55 // 4. Sort: upcoming first 56 allRows.sort((a, b) => { 57 const order = (s) => s === '未开始' ? 0 : s === '进行中' ? 1 : s === '已结束' ? 2 : s === '已完成' ? 3 : 4; 58 return order(a.status) - order(b.status); 59 }); 60 // 5. Filter by status 61 const statusMap = { 62 upcoming: ['未开始'], 63 ongoing: ['进行中'], 64 finished: ['已结束', '已完成'], 65 }; 66 const finalRows = statusFilter === 'all' 67 ? allRows 68 : allRows.filter(r => statusMap[statusFilter]?.includes(r.status)); 69 return finalRows.slice(0, Number(limit)).map((item, i) => ({ 70 rank: i + 1, 71 ...item, 72 })); 73 }, 74 });