// ==UserScript== // @name TG任务状态后台扫描器 // @namespace tg-task-monitor // @version 2.0.0 // @description 后台定时扫描 tg.zcst.edu.cn 课堂考试、小测试和图文作业状态,并保存到共享存储 // @author ChatGPT // @storageName tg-exam-monitor-shared // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_log // @connect tg.zcst.edu.cn // @crontab */30 * * * * // ==/UserScript== /** * TG任务状态后台扫描器 * * 功能: * 1. 后台请求所有正在进行的课堂 * 2. 保留原考试 / 小测试 exercises 扫描逻辑 * 3. 新增 common_homework 图文作业扫描逻辑 * 4. 合并为 courses + tasks 统一结构并保存到共享存储 * * 注意: * - 需要当前浏览器已经登录 tg.zcst.edu.cn * - 后台脚本依赖浏览器 Cookie 登录态 * - 前台面板脚本通过相同 @storageName 读取结果 */ const MANUAL_LOGIN = ""; const BASE = "https://tg.zcst.edu.cn"; const STORE_KEY = "TG_EXAM_MONITOR_RESULT"; const STORE_KEY_LAST_ERROR = "TG_EXAM_MONITOR_LAST_ERROR"; const STORE_KEY_LAST_RUNNING = "TG_EXAM_MONITOR_LAST_RUNNING"; const STORE_KEY_LAST_NOTIFY_STATE = "TG_TASK_MONITOR_LAST_NOTIFY_STATE"; const STORE_KEY_AUTO_LOGIN = "TG_TASK_MONITOR_AUTO_LOGIN"; const STORE_KEY_REFRESH_REQUEST = "TG_TASK_REFRESH_REQUEST"; const STORE_KEY_REFRESH_HANDLED = "TG_TASK_REFRESH_HANDLED"; const COURSE_PAGE_SIZE = 30; const EXERCISE_LIMIT = 100; const HOMEWORK_LIMIT = 100; const REQUEST_INTERVAL_MS = 150; const COURSE_INTERVAL_MS = 300; async function getCurrentLogin() { const manualLogin = String(MANUAL_LOGIN || "").trim(); if (manualLogin) { return manualLogin; } const savedLogin = String(GM_getValue(STORE_KEY_AUTO_LOGIN, "") || "").trim(); if (savedLogin) { return savedLogin; } throw new Error("无法自动识别 TG 登录账号,请先打开 TG 页面让前台识别,或在后台脚本顶部填写 MANUAL_LOGIN,例如:user_04241025"); } function logInfo(...args) { const text = args.map(formatLogArg).join(" "); if (typeof GM_log === "function") GM_log(text, "info"); console.log("[TG任务状态后台扫描器]", ...args); } function logWarn(...args) { const text = args.map(formatLogArg).join(" "); if (typeof GM_log === "function") GM_log(text, "warn"); console.warn("[TG任务状态后台扫描器]", ...args); } function logError(...args) { const text = args.map(formatLogArg).join(" "); if (typeof GM_log === "function") GM_log(text, "error"); console.error("[TG任务状态后台扫描器]", ...args); } function formatLogArg(arg) { if (typeof arg === "string") return arg; try { return JSON.stringify(arg); } catch (e) { return String(arg); } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function setValue(key, value) { if (typeof GM !== "undefined" && GM.setValue) { await GM.setValue(key, value); return; } GM_setValue(key, value); } async function getValue(key, defaultValue) { if (typeof GM !== "undefined" && GM.getValue) { return await GM.getValue(key, defaultValue); } return GM_getValue(key, defaultValue); } async function markRefreshRequestHandledIfNeeded() { const request = await getValue(STORE_KEY_REFRESH_REQUEST, null); const handled = Number(await getValue(STORE_KEY_REFRESH_HANDLED, 0)) || 0; const requestedAt = Number(request?.requestedAt) || 0; if (requestedAt && requestedAt > handled) { await setValue(STORE_KEY_REFRESH_HANDLED, requestedAt); } } function requestJson(url, options = {}) { const method = options.method || "GET"; const isPost = method.toUpperCase() === "POST"; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method, url, anonymous: false, withCredentials: true, headers: isPost ? { "_method": "POST", "accept": "application/json", "content-type": "application/json; charset=utf-8", "x-http-method-override": "POST" } : { "accept": "application/json", "content-type": "application/json; charset=utf-8", "x-http-method-override": "GET" }, data: options.body ? JSON.stringify(options.body) : undefined, timeout: 30000, onload(res) { const status = res.status; const text = res.responseText || ""; if (status < 200 || status >= 300) { reject(new Error(`HTTP ${status}: ${url}\n${text.slice(0, 500)}`)); return; } try { resolve(text ? JSON.parse(text) : null); } catch (e) { reject(new Error(`JSON解析失败: ${url}\n${text.slice(0, 500)}`)); } }, ontimeout() { reject(new Error(`请求超时: ${url}`)); }, onerror(err) { reject(new Error(`请求失败: ${url}\n${String(err)}`)); } }); }); } function normalizeText(text) { return String(text ?? "").replace(/\s+/g, " ").trim(); } function firstPresent(...values) { for (const value of values) { if (value !== undefined && value !== null && value !== "") return value; } return null; } function looksLikeNumericId(value) { return typeof value === "number" || (typeof value === "string" && /^\d+$/.test(value)); } function getNumericCourseId(course) { const candidates = [ course.courseId, course.course_id, course.id, course.courses_id, course.course?.id, course.raw?.id ]; const value = candidates.find(looksLikeNumericId); return value === undefined ? null : value; } function extractCourseIdentifier(course) { if (!course || typeof course !== "object") return null; const directCandidates = [ course.courseIdentifier, course.identifier, course.course_identifier, course.invite_code, course.inviteCode, course.course_code, course.courseCode, course.classroom_identifier, course.classroomIdentifier ]; for (const v of directCandidates) { if (typeof v === "string" && v.trim()) { return v.trim(); } } const urlCandidates = [ course.first_category_url, course.classroom_url, course.course_url, course.url, course.home_url ]; for (const url of urlCandidates) { if (typeof url === "string") { const match = url.match(/\/classrooms\/([^/]+)/); if (match && match[1]) { return match[1]; } } } const text = JSON.stringify(course); const match = text.match(/\/classrooms\/([^/]+)/); if (match && match[1]) { return match[1]; } return null; } function getCourseName(course) { return normalizeText( firstPresent( course.name, course.course_name, course.courseName, course.title, course.subject, course.course_title, course.course?.name, "未识别课堂名" ) ); } function normalizeCourse(course) { return { courseId: getNumericCourseId(course), courseIdentifier: extractCourseIdentifier(course), courseName: getCourseName(course), raw: course }; } function parseCourseList(raw) { const list = raw?.courses || raw?.data?.courses || raw?.data?.course_list || raw?.data?.list || raw?.data || []; return Array.isArray(list) ? list : []; } function parseExamList(raw) { const list = raw?.exercises || raw?.data?.exercises || raw?.data?.list || raw?.data || []; return Array.isArray(list) ? list : []; } function parseHomeworkList(raw) { const list = raw?.homeworks || raw?.homework_commons || raw?.data?.homeworks || raw?.data?.items || raw?.data?.homework_commons || raw?.data?.list || raw?.data || []; return Array.isArray(list) ? list : []; } function parseExerciseCommitStatus(user) { const commitStatus = user?.commit_status; if (commitStatus === 0) { return { commitStatus, statusText: "未开始/未提交", completed: false }; } if (commitStatus === 1) { return { commitStatus, statusText: "进行中/未提交", completed: false }; } if (commitStatus === 2) { return { commitStatus, statusText: "已完成/已提交", completed: true }; } return { commitStatus, statusText: `未知状态码:${commitStatus}`, completed: !!user?.end_at || (user?.score !== undefined && user?.score !== null && user?.score !== "--") }; } function parseCommonHomework(data, fallback = {}) { const studentWorks = Array.isArray(data?.student_works) ? data.student_works : []; const homeworkStatus = Array.isArray(data?.homework_status) ? data.homework_status : []; const isEnded = homeworkStatus.includes("已截止"); const isNotSubmitted = data?.work_status === 0 && Number(data?.submit_count || 0) === 0 && !data?.update_time && studentWorks.length === 0; const isSubmitted = Number(data?.submit_count || 0) > 0 || !!data?.update_time || studentWorks.length > 0 || data?.work_status === 1 || data?.work_status === 2; let statusText = "未知状态"; if (isNotSubmitted) { statusText = isEnded ? "已截止/未提交" : "未提交"; } else if (isSubmitted) { statusText = isEnded ? "已截止/已提交" : "已提交"; } const courseIdentifier = fallback.courseIdentifier || data?.course_identifier || data?.coursesId || null; return { taskType: "common_homework", taskTypeText: "图文作业", courseId: data?.course_id ?? fallback.courseId ?? null, courseIdentifier, courseName: normalizeText(data?.course_name || fallback.courseName || "未识别课堂名"), homeworkId: data?.homework_id ?? fallback.homeworkId ?? null, workId: data?.work_id || data?.id || null, title: normalizeText(data?.homework_name || fallback.title || "未识别图文作业名"), categoryId: data?.category?.category_id ?? null, categoryName: data?.category?.category_name || "图文作业", homeworkStatus, timeStatus: data?.time_status, statusText, completed: isSubmitted, submitted: isSubmitted, notSubmitted: isNotSubmitted, ended: isEnded, canSubmit: !!data?.can_submit, publishTime: data?.publish_time || "", endTime: data?.end_time || "", lateTime: data?.late_time || "", score: data?.final_score || data?.work_score || "--", workScore: data?.work_score, finalScore: data?.final_score, teacherScore: data?.teacher_score, studentScore: data?.student_score, submitCount: data?.submit_count ?? 0, commitCount: data?.commit_count, uncommitCount: data?.uncommit_count, userLogin: data?.user_login || "", studentId: data?.student_id || "", userName: data?.user_name || "", groupName: data?.group_name || "", scanTime: new Date().toLocaleString(), raw: data }; } function parseCommonHomeworkFromListItem(courseResult, item) { const homeworkStatus = Array.isArray(item?.status) ? item.status : []; const workStatus = Array.isArray(item?.work_status) ? item.work_status : []; const workStatusText = workStatus.join(" "); const ended = homeworkStatus.includes("已截止") || item?.time_status === 5; let notSubmitted = false; let submitted = false; if (typeof item?.un_commit_work === "boolean") { notSubmitted = item.un_commit_work === true; submitted = item.un_commit_work === false; } else { notSubmitted = workStatusText.includes("提交作品") || workStatusText.includes("未提交"); submitted = workStatusText.includes("查看作品") || workStatusText.includes("已提交") || workStatusText.includes("已完成"); } let statusText = "未知状态"; if (notSubmitted) { statusText = ended ? "已截止/未提交" : "未提交"; } else if (submitted) { statusText = ended ? "已截止/已提交" : "已提交"; } return { taskType: "common_homework", taskTypeText: "图文作业", courseId: courseResult.courseId, courseIdentifier: courseResult.courseIdentifier, courseName: courseResult.courseName, homeworkId: item?.homework_id || item?.id, workId: item?.work_id || item?.student_work_id, studentWorkId: item?.student_work_id, title: item?.name || item?.homework_name || "未命名图文作业", homeworkStatus, workStatus, timeStatus: item?.time_status, statusText, completed: submitted, submitted, notSubmitted, ended, publishTime: item?.publish_time || "", endTime: item?.end_time || item?.end_time_s || "", lateTime: item?.late_time || "", score: "--", source: "homework_commons_list", raw: item }; } function parseClassroomExperimentFromListItem(courseResult, item) { const finishedStatus = Number(item?.shixun_finished_status); const completed = finishedStatus === 1; const ended = item?.time_status === 5 || (Array.isArray(item?.status) && item.status.includes("已截止")); let statusText = "未知状态"; if (completed) { statusText = "已完成"; } else if (ended) { statusText = "已截止/未完成"; } else { statusText = "进行中/未完成"; } const homeworkId = item?.homework_id || item?.id; return { taskType: "classroom_experiment", taskTypeText: "课堂实验", courseId: courseResult.courseId, courseIdentifier: courseResult.courseIdentifier, courseName: courseResult.courseName, homeworkId, title: item?.name || item?.shixun_name || "未命名课堂实验", statusText, completed, submitted: completed, notSubmitted: !completed, ended, publishTime: item?.publish_time || "", endTime: item?.end_time_s || item?.end_time || "", lateTime: item?.late_time || "", challengeCount: item?.challenge_count ?? null, finishedChallengeCount: item?.finished_challenge_count ?? null, passedTime: item?.student_passed_time || "", shixunIdentifier: item?.shixun_identifier || "", myshixunIdentifier: item?.myshixun_identifier || "", taskOperation: item?.task_operation || [], detailUrl: buildClassroomExperimentDetailUrl(courseResult.courseIdentifier, homeworkId), source: "homework_commons_type_4", raw: item }; } function buildCoursesUrl(page, login) { return ( `${BASE}/api/users/${encodeURIComponent(login)}/courses.json` + `?category=&status=processing` + `&page=${page}` + `&per_page=${COURSE_PAGE_SIZE}` + `&sort_by=updated_at` + `&sort_direction=desc` + `&username=${encodeURIComponent(login)}` + `&zzud=${encodeURIComponent(login)}` + `&_t=${Date.now()}` ); } function buildExercisesUrl(courseId, login) { return ( `${BASE}/api/v2/courses/${courseId}/exercises.json` + `?coursesId=${encodeURIComponent(courseId)}` + `&limit=${EXERCISE_LIMIT}` + `&type=` + `&id=${encodeURIComponent(courseId)}` + `&zzud=${encodeURIComponent(login)}` + `&_t=${Date.now()}` ); } function buildExerciseUsersUrl(courseId, exerciseId, login) { return ( `${BASE}/api/exercises/${exerciseId}/exercise_users.json` + `?page=1` + `&limit=20` + `&coursesId=${encodeURIComponent(courseId)}` + `&categoryId=${encodeURIComponent(exerciseId)}` + `&zzud=${encodeURIComponent(login)}` + `&_t=${Date.now()}` ); } function buildHomeworkCommonsUrl(courseIdentifier, login) { return ( `${BASE}/api/courses/${encodeURIComponent(courseIdentifier)}/homework_commons.json` + `?coursesId=${encodeURIComponent(courseIdentifier)}` + `&id=${encodeURIComponent(courseIdentifier)}` + `&limit=${HOMEWORK_LIMIT}` + `&type=1` + `&zzud=${encodeURIComponent(login)}` + `&_t=${Date.now()}` ); } function buildHomeworkWorksListUrl(homeworkId) { return `${BASE}/api/homework_commons/${encodeURIComponent(homeworkId)}/works_list.json?_t=${Date.now()}`; } function buildExerciseDetailUrl(courseIdentifier, exerciseId, login) { if (!courseIdentifier || !exerciseId || !login) return ""; return `${BASE}/classrooms/${courseIdentifier}/exercisenotice/${exerciseId}/users/${encodeURIComponent(login)}`; } function buildHomeworkDetailUrl(courseIdentifier, homeworkId) { if (!courseIdentifier || !homeworkId) return ""; return `${BASE}/classrooms/${courseIdentifier}/common_homework/${homeworkId}/detail?tabs=0`; } function buildClassroomExperimentDetailUrl(courseIdentifier, homeworkId) { if (!courseIdentifier || !homeworkId) return ""; return `${BASE}/classrooms/${courseIdentifier}/shixun_homework/${homeworkId}/detail?tabs=1`; } async function fetchAllCourses(login) { const allCourses = []; let page = 1; let totalCount = null; while (page <= 10) { const url = buildCoursesUrl(page, login); logInfo(`请求课堂列表 page=${page}`); const raw = await requestJson(url); const list = parseCourseList(raw); if (typeof raw?.total_count === "number") totalCount = raw.total_count; allCourses.push(...list); if (list.length < COURSE_PAGE_SIZE) break; if (totalCount !== null && allCourses.length >= totalCount) break; page++; await sleep(REQUEST_INTERVAL_MS); } return allCourses.map(normalizeCourse); } async function fetchExercises(courseId, login) { const raw = await requestJson(buildExercisesUrl(courseId, login)); return { raw, exams: parseExamList(raw) }; } async function fetchExerciseUser(courseId, exerciseId, login) { const raw = await requestJson(buildExerciseUsersUrl(courseId, exerciseId, login)); return { raw, user: raw?.data?.current_answer_user || null, totalScore: raw?.data?.total_score ?? "--" }; } async function fetchCommonHomeworkList(courseIdentifier, login) { return await requestJson(buildHomeworkCommonsUrl(courseIdentifier, login)); } function buildHomeworkCommonsByTypeUrl(courseIdentifier, login, type) { return ( `${BASE}/api/courses/${encodeURIComponent(courseIdentifier)}/homework_commons.json` + `?limit=100` + `&status=0` + `&id=${encodeURIComponent(courseIdentifier)}` + `&type=${encodeURIComponent(type)}` + `&sort_by=updated_at` + `&sort_direction=asc` + `&order=0` + `&zzud=${encodeURIComponent(login)}` + `&_t=${Date.now()}` ); } async function fetchHomeworkCommonsByType(courseResult, login, type) { const courseIdentifier = courseResult.courseIdentifier; if (!courseIdentifier) return []; const data = await requestJson(buildHomeworkCommonsByTypeUrl(courseIdentifier, login, type)); return Array.isArray(data?.homeworks) ? data.homeworks : []; } async function fetchCommonHomeworkStatus(courseIdentifier, homeworkId) { const url = `https://tg.zcst.edu.cn/api/homework_commons/${homeworkId}/works_list.json`; const res = await fetch(url, { method: "POST", credentials: "include", headers: { "_method": "POST", "accept": "application/json", "content-type": "application/json; charset=utf-8", "x-http-method-override": "POST" }, body: JSON.stringify({ coursesId: String(courseIdentifier), categoryId: String(homeworkId) }) }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`works_list 请求失败: HTTP ${res.status} ${res.statusText} ${text.slice(0, 200)}`); } return await res.json(); } function getHomeworkId(homework) { return firstPresent(homework.homework_id, homework.id, homework.category_id); } function getHomeworkTitle(homework) { return normalizeText(homework.homework_name || homework.name || homework.title || "未识别图文作业名"); } function createExerciseTask(course, exam, user, totalScore, parsed, login) { const exerciseId = exam.id; return { taskType: "exercise", taskTypeText: "考试/小测试", courseId: course.courseId, courseIdentifier: course.courseIdentifier, courseName: course.courseName, exerciseId, title: normalizeText(exam.exercise_name || exam.name || exam.title || "未识别考试名"), detailUrl: buildExerciseDetailUrl(course.courseIdentifier, exerciseId, login), deadlineRemaining: exam.exercise_left_time || "--", durationMinutes: exam.time ?? null, exerciseStatus: exam.exercise_status ?? null, currentStatus: exam.current_status ?? null, wholeExerciseStatus: exam.whole_exercise_status ?? null, exerciseUserId: exam.exercise_user_id ?? user?.exercise_user_id ?? null, ...parsed, startAt: user?.start_at || null, endAt: user?.end_at || null, score: user?.score ?? "--", totalScore, objectiveScore: user?.objective_score ?? "--", subjectiveScore: user?.subjective_score ?? "--", reviewStatus: user?.review_status ?? false, isForceCommit: user?.is_force_commit ?? false, userName: user?.user_name || "", studentId: user?.student_id || "", scanTime: new Date().toLocaleString(), raw: { exam, user } }; } async function scanExercisesForCourse(course, result, courseResult, login) { if (!course.courseId) { const warning = { stage: "course", courseName: course.courseName, message: "未识别数字 courseId,跳过考试/小测试扫描", course: course.raw }; result.warnings.push(warning); logWarn(warning.message, course.courseName); return; } try { const { exams } = await fetchExercises(course.courseId, login); logInfo(`课堂 ${course.courseName} 考试数量:${exams.length}`); for (const exam of exams) { const exerciseId = exam.id; const title = normalizeText(exam.exercise_name || exam.name || exam.title || "未识别考试名"); if (!exerciseId) { result.errors.push({ stage: "exam", courseId: course.courseId, courseName: course.courseName, message: "未识别 exerciseId", exam }); continue; } let user = null; let totalScore = "--"; let parsed = {}; try { const userResult = await fetchExerciseUser(course.courseId, exerciseId, login); user = userResult.user; totalScore = userResult.totalScore; parsed = parseExerciseCommitStatus(user); } catch (err) { parsed = { commitStatus: null, statusText: "状态读取失败", completed: false, error: String(err) }; result.errors.push({ stage: "exercise_user", courseId: course.courseId, courseName: course.courseName, exerciseId, title, error: String(err) }); } const task = createExerciseTask(course, exam, user, totalScore, parsed, login); courseResult.exams.push(task); result.tasks.push(task); await sleep(REQUEST_INTERVAL_MS); } } catch (err) { result.errors.push({ stage: "exercises", courseId: course.courseId, courseName: course.courseName, error: String(err) }); } } async function scanCommonHomeworksForCourse(course, result, courseResult, login) { if (!course.courseIdentifier) { const warning = { stage: "homework_commons", courseId: course.courseId, courseName: course.courseName, message: "未识别课堂短 ID,跳过图文作业扫描", course: course.raw }; result.warnings.push(warning); result.debug.homeworkScan.push({ courseName: course.courseName, courseId: course.courseId, courseIdentifier: course.courseIdentifier, status: "skipped", reason: "missing_course_identifier", homeworkListCount: 0, homeworkIds: [], parsedHomeworkCount: courseResult.homeworks.length }); logWarn(warning.message, course.courseName); return; } try { const listData = await fetchCommonHomeworkList(course.courseIdentifier, login); const homeworkItems = Array.isArray(listData?.homeworks) ? listData.homeworks : []; logInfo(`课堂 ${course.courseName} 图文作业数量:${homeworkItems.length}`); const parsedListTasks = homeworkItems.map(item => { const task = parseCommonHomeworkFromListItem(courseResult, item); task.detailUrl = buildHomeworkDetailUrl(course.courseIdentifier, task.homeworkId); return task; }); for (const task of parsedListTasks) { courseResult.homeworks.push(task); result.tasks.push(task); } result.debug.homeworkScan.push({ courseName: course.courseName, courseId: course.courseId, courseIdentifier: course.courseIdentifier, status: "success", homeworkListCount: homeworkItems.length, homeworkIds: homeworkItems.map(x => x?.homework_id || x?.id), parsedHomeworkCount: parsedListTasks.length, source: "homework_commons_list" }); } catch (err) { result.errors.push({ stage: "homework_commons", courseId: course.courseId, courseIdentifier: course.courseIdentifier, courseName: course.courseName, error: String(err) }); result.debug.homeworkScan.push({ courseName: course.courseName, courseId: course.courseId, courseIdentifier: course.courseIdentifier, status: "failed", error: String(err), homeworkListCount: 0, homeworkIds: [], parsedHomeworkCount: courseResult.homeworks.length }); } } async function scanClassroomExperimentsForCourse(courseResult, result, login) { result.debug = result.debug || {}; result.debug.experimentScan = result.debug.experimentScan || []; if (!courseResult.courseIdentifier) { result.debug.experimentScan.push({ courseId: courseResult.courseId, courseIdentifier: courseResult.courseIdentifier, courseName: courseResult.courseName, status: "skipped", reason: "missing_course_identifier", count: 0 }); return; } try { const experimentList = await fetchHomeworkCommonsByType(courseResult, login, 4); const experiments = experimentList.map(item => parseClassroomExperimentFromListItem(courseResult, item) ); courseResult.experiments = experiments; for (const task of experiments) { result.tasks.push(task); } result.debug.experimentScan.push({ courseId: courseResult.courseId, courseIdentifier: courseResult.courseIdentifier, courseName: courseResult.courseName, status: "success", count: experiments.length, rawCount: experimentList.length }); } catch (error) { courseResult.experiments = []; result.errors.push({ stage: "classroom_experiment", courseId: courseResult.courseId, courseIdentifier: courseResult.courseIdentifier, courseName: courseResult.courseName, message: error?.message || String(error) }); result.debug.experimentScan.push({ courseId: courseResult.courseId, courseIdentifier: courseResult.courseIdentifier, courseName: courseResult.courseName, status: "failed", error: error?.message || String(error), stack: error?.stack || "" }); } } function getTaskEndDate(task) { const candidates = [ task?.endTime, task?.endAt ]; for (const value of candidates) { if (!value) continue; const d = new Date(String(value).replace(/-/g, "/")); if (!Number.isNaN(d.getTime())) return d; } return null; } function isDangerTask(task) { if (task?.completed) return false; if (task?.ended) return false; const text = String(task?.statusText || ""); const endDate = getTaskEndDate(task); const now = new Date(); if (endDate) { if (endDate < now) return false; const diffDays = (endDate.getTime() - now.getTime()) / 86400000; if (diffDays <= 10) return true; } return task?.taskType === "exercise" && text.includes("进行中") && !task.ended; } function sortTasks(tasks) { const priority = task => { if (isDangerTask(task)) return 1; if (!task.completed && task.ended) return 2; if (!task.completed) return 3; return 5; }; return [...tasks].sort((a, b) => priority(a) - priority(b)); } function summarizeResult(result) { const tasks = result.tasks || []; const exerciseCount = tasks.filter(task => task.taskType === "exercise").length; const homeworkCount = tasks.filter(task => task.taskType === "common_homework").length; const experimentCount = tasks.filter(task => task.taskType === "classroom_experiment").length; const completedCount = tasks.filter(task => task.completed).length; const unfinishedCount = tasks.filter(task => !task.completed).length; const runningCount = tasks.filter(task => String(task.statusText || "").includes("进行中")).length; const dangerCount = tasks.filter(isDangerTask).length; return { courseCount: result.courses.length, taskCount: tasks.length, examCount: exerciseCount, exerciseCount, homeworkCount, experimentCount, completedCount, unfinishedCount, runningCount, dangerCount, errorCount: result.errors.length, warningCount: result.warnings.length }; } async function scanAll() { const startedAt = new Date(); const startTime = startedAt.toLocaleString(); let login = ""; let currentStage = "init"; let totalCourses = 0; await markRefreshRequestHandledIfNeeded(); await setValue(STORE_KEY_LAST_RUNNING, { running: true, startTime, timestamp: startedAt.getTime(), stage: "fetch_courses", stageText: "正在获取课程列表", current: 0, total: 0, percent: 5 }); await setValue(STORE_KEY_LAST_ERROR, null); try { currentStage = "getCurrentLogin"; login = await getCurrentLogin(); } catch (err) { const message = err?.message || String(err); await setValue(STORE_KEY_LAST_ERROR, { time: startTime, message, stack: err?.stack || "", stage: "getCurrentLogin" }); await setValue(STORE_KEY_LAST_RUNNING, { running: false, endTime: new Date().toLocaleString(), timestamp: Date.now(), stage: "failed", stageText: "扫描失败", current: 0, total: 0, percent: 100 }); logError(message); return null; } const result = { login, scanTime: startTime, scanTimestamp: startedAt.getTime(), source: "scriptcat-background", courses: [], tasks: [], errors: [], warnings: [], debug: { homeworkScan: [], experimentScan: [], loginSource: { manualLoginConfigured: Boolean(String(MANUAL_LOGIN || "").trim()), autoLoginFromStorage: String(GM_getValue(STORE_KEY_AUTO_LOGIN, "") || "").trim(), resolvedLogin: login } }, summary: { courseCount: 0, taskCount: 0, examCount: 0, exerciseCount: 0, homeworkCount: 0, experimentCount: 0, completedCount: 0, unfinishedCount: 0, runningCount: 0, dangerCount: 0, errorCount: 0, warningCount: 0 } }; logInfo("开始扫描"); try { currentStage = "courses"; const courses = await fetchAllCourses(login); totalCourses = courses.length; logInfo(`识别到课堂数量:${courses.length}`); for (let index = 0; index < courses.length; index += 1) { const course = courses[index]; currentStage = `course:${course.courseName || course.courseId || "unknown"}`; await setValue(STORE_KEY_LAST_RUNNING, { running: true, startTime, timestamp: Date.now(), stage: "scan_course", stageText: `正在扫描:${course.courseName || course.courseId || "未识别课程"}`, current: index + 1, total: courses.length, percent: Math.max(5, Math.round(((index + 1) / Math.max(courses.length, 1)) * 90)) }); const courseResult = { courseId: course.courseId, courseIdentifier: course.courseIdentifier, courseName: course.courseName, exams: [], homeworks: [], experiments: [], raw: course.raw }; logInfo(`扫描课堂:${course.courseName} / courseId=${course.courseId || "--"} / courseIdentifier=${course.courseIdentifier || "--"}`); await scanExercisesForCourse(course, result, courseResult, login); await scanCommonHomeworksForCourse(course, result, courseResult, login); await scanClassroomExperimentsForCourse(courseResult, result, login); result.courses.push(courseResult); await sleep(COURSE_INTERVAL_MS); } result.tasks = sortTasks(result.tasks); result.summary = summarizeResult(result); await setValue(STORE_KEY, result); await setValue(STORE_KEY_LAST_ERROR, null); await setValue(STORE_KEY_LAST_RUNNING, { running: false, endTime: new Date().toLocaleString(), timestamp: Date.now(), stage: "done", stageText: "扫描完成", current: totalCourses, total: totalCourses, percent: 100 }); logInfo("扫描完成", result.summary); return result; } catch (err) { result.errors.push({ stage: "main", error: String(err) }); result.tasks = sortTasks(result.tasks); result.summary = summarizeResult(result); await setValue(STORE_KEY, result); await setValue(STORE_KEY_LAST_ERROR, { time: new Date().toLocaleString(), message: err?.message || String(err), stack: err?.stack || "", stage: currentStage || "unknown" }); await setValue(STORE_KEY_LAST_RUNNING, { running: false, endTime: new Date().toLocaleString(), timestamp: Date.now(), stage: "failed", stageText: "扫描失败", current: 0, total: totalCourses, percent: 100 }); logError("扫描失败", err); return result; } } scanAll();