// 课表以空 HTML 表格返回,课程数据通过 JavaScript 脚本动态注入 // 脚本中包含 `new TaskActivity(...)` 构造函数调用来定义课程 // 需要从脚本文本中直接提取课程信息,而不是解析 DOM (function () { const BASE = "https://jwc3-yangtzeu-edu-cn-s.atrust.yangtzeu.edu.cn"; function extractCourseHtmlDebugInfo(courseHtml) { const text = String(courseHtml || ""); const hasTaskActivity = /new\s+TaskActivity\s*\(/i.test(text); return { responseLength: text.length, hasTaskActivity }; } async function requestText(url, options) { const requestOptions = { credentials: "include", ...options }; const res = await fetch(url, requestOptions); const text = await res.text(); if (!res.ok) { throw new Error(`网络请求失败: ${res.status}`); } return text; } // 从入口页提取学生 ID 和学期组件 tagId function parseEntryParams(entryHtml) { const idsMatch = entryHtml.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/); const tagIdMatch = entryHtml.match(/id="(semesterBar\d+Semester)"/); return { studentId: idsMatch ? idsMatch[1] : "", tagId: tagIdMatch ? tagIdMatch[1] : "" }; } // 学期接口返回对象字面量,这里按脚本文本解析 function parseSemesterResponse(rawText) { let data; try { data = Function(`return (${String(rawText || "").trim()});`)(); } catch (_) { throw new Error("学期数据解析失败"); } const semesters = []; if (!data || !data.semesters || typeof data.semesters !== "object") { return semesters; } Object.keys(data.semesters).forEach((k) => { const arr = data.semesters[k]; if (!Array.isArray(arr)) return; arr.forEach((s) => { if (!s || !s.id) return; semesters.push({ id: String(s.id), name: `${s.schoolYear || ""} 第${s.name || ""}学期`.trim() }); }); }); return semesters; } // 清除课程名后面的课程序号 function cleanCourseName(name) { return String(name || "").replace(/\(\d+\)\s*$/, "").trim(); } // 解析周次位图字符串 function parseValidWeeksBitmap(bitmap) { if (!bitmap || typeof bitmap !== "string") return []; const weeks = []; for (let i = 0; i < bitmap.length; i++) { if (bitmap[i] === "1" && i >= 1) weeks.push(i); } return weeks; } function normalizeWeeks(weeks) { const list = Array.from(new Set((weeks || []).filter((w) => Number.isInteger(w) && w > 0))); list.sort((a, b) => a - b); return list; } // 节次编号与 TimeSlots 编号映射 function mapSectionToTimeSlotNumber(section) { const mapping = { 1: 1, 2: 2, 3: 4, 4: 5, 5: 7, 6: 8, 7: 3, 8: 6 }; return mapping[section] || section; } // 反引号化 JavaScript 字面量字符串,处理转义字符 function unquoteJsLiteral(token) { const text = String(token || "").trim(); if (!text) return ""; if (text === "null" || text === "undefined") return ""; if ((text.startsWith("\"") && text.endsWith("\"")) || (text.startsWith("'") && text.endsWith("'"))) { const quote = text[0]; let inner = text.slice(1, -1); inner = inner .replace(/\\\\/g, "\\") .replace(new RegExp(`\\\\${quote}`, "g"), quote) .replace(/\\n/g, "\n") .replace(/\\r/g, "\r") .replace(/\\t/g, "\t"); return inner; } return text; } // 分割 JavaScript 函数参数字符串,正确处理引号和转义 function splitJsArgs(argsText) { const args = []; let curr = ""; let inQuote = ""; let escaped = false; for (let i = 0; i < argsText.length; i++) { const ch = argsText[i]; if (escaped) { curr += ch; escaped = false; continue; } if (ch === "\\") { curr += ch; escaped = true; continue; } if (inQuote) { curr += ch; if (ch === inQuote) inQuote = ""; continue; } if (ch === "\"" || ch === "'") { curr += ch; inQuote = ch; continue; } if (ch === ",") { args.push(curr.trim()); curr = ""; continue; } curr += ch; } if (curr.trim() || argsText.endsWith(",")) { args.push(curr.trim()); } return args; } // 从脚本文本中的 TaskActivity 还原课程 function parseCoursesFromTaskActivityScript(htmlText) { const text = String(htmlText || ""); if (!text) return []; const unitCountMatch = text.match(/\bvar\s+unitCount\s*=\s*(\d+)\s*;/); const unitCount = unitCountMatch ? parseInt(unitCountMatch[1], 10) : 0; if (!Number.isInteger(unitCount) || unitCount <= 0) return []; const courses = []; const stats = { blocks: 0, teacherRecovered: 0, teacherUnresolvedExpression: 0 }; const blockRe = /activity\s*=\s*new\s+TaskActivity\(([^]*?)\)\s*;\s*index\s*=\s*(?:(\d+)\s*\*\s*unitCount\s*\+\s*(\d+)|(\d+))\s*;\s*table\d+\.activities\[index\]/g; let match; while ((match = blockRe.exec(text)) !== null) { stats.blocks += 1; const argsText = match[1] || ""; const args = splitJsArgs(argsText); if (args.length < 7) continue; const dayPart = match[2]; const sectionPart = match[3]; const directIndexPart = match[4]; let indexValue = -1; if (dayPart != null && sectionPart != null) { indexValue = parseInt(dayPart, 10) * unitCount + parseInt(sectionPart, 10); } else if (directIndexPart != null) { indexValue = parseInt(directIndexPart, 10); } if (!Number.isInteger(indexValue) || indexValue < 0) continue; const day = Math.floor(indexValue / unitCount) + 1; let section = (indexValue % unitCount) + 1; section = mapSectionToTimeSlotNumber(section); if (day < 1 || day > 7 || section < 1 || section > 16) continue; let teacher = unquoteJsLiteral(args[1]); if (teacher && !/^['"]/.test(String(args[1]).trim()) && /join\s*\(/.test(String(args[1]))) { const resolved = resolveTeachersForTaskActivityBlock(text, match.index); if (resolved) { teacher = resolved; stats.teacherRecovered += 1; } else { stats.teacherUnresolvedExpression += 1; } } const name = cleanCourseName(unquoteJsLiteral(args[3])); const position = unquoteJsLiteral(args[5]); const weekBitmap = unquoteJsLiteral(args[6]); const weeks = normalizeWeeks(parseValidWeeksBitmap(weekBitmap)); if (!name) continue; courses.push({ name, teacher, position, day, startSection: section, endSection: section, weeks }); } console.info("[课程解析 TaskActivity]", { blocks: stats.blocks, parsedCourses: courses.length, teacherRecovered: stats.teacherRecovered, teacherUnresolvedExpression: stats.teacherUnresolvedExpression }); return mergeContiguousSections(courses); } // 当教师名为表达式时,尝试在附近代码中回溯真实教师名 function resolveTeachersForTaskActivityBlock(fullText, blockStartIndex) { const start = Math.max(0, blockStartIndex - 2200); const segment = fullText.slice(start, blockStartIndex); const re = /var\s+actTeachers\s*=\s*\[([^]*?)\]\s*;/g; let m; let last = null; while ((m = re.exec(segment)) !== null) { last = m[1]; } if (!last) return ""; const names = []; const nameRe = /name\s*:\s*(?:"([^"]*)"|'([^']*)')/g; let nm; while ((nm = nameRe.exec(last)) !== null) { const name = (nm[1] || nm[2] || "").trim(); if (name) names.push(name); } if (names.length === 0) return ""; return Array.from(new Set(names)).join(","); } // 合并同一课程的连续节次 function mergeContiguousSections(courses) { const list = (courses || []) .filter((c) => c && c.name && Number.isInteger(c.day) && Number.isInteger(c.startSection) && Number.isInteger(c.endSection)) .map((c) => ({ ...c, weeks: normalizeWeeks(c.weeks) })); list.sort((a, b) => { const ak = `${a.name}|${a.teacher}|${a.position}|${a.day}|${a.weeks.join(",")}`; const bk = `${b.name}|${b.teacher}|${b.position}|${b.day}|${b.weeks.join(",")}`; if (ak < bk) return -1; if (ak > bk) return 1; return a.startSection - b.startSection; }); const merged = []; for (const item of list) { const prev = merged[merged.length - 1]; const canMerge = prev && prev.name === item.name && prev.teacher === item.teacher && prev.position === item.position && prev.day === item.day && prev.weeks.join(",") === item.weeks.join(",") && prev.endSection + 1 >= item.startSection; if (canMerge) { prev.endSection = Math.max(prev.endSection, item.endSection); } else { merged.push({ ...item }); } } return merged; } function getPresetTimeSlots() { return [ { number: 1, startTime: "08:00", endTime: "09:35" }, { number: 2, startTime: "10:05", endTime: "11:40" }, { number: 3, startTime: "12:00", endTime: "13:35" }, // 午间课 { number: 4, startTime: "14:00", endTime: "15:35" }, { number: 5, startTime: "16:05", endTime: "17:40" }, { number: 6, startTime: "17:45", endTime: "18:30" }, // 晚间课,部分课程为 18:00-18:45 { number: 7, startTime: "19:00", endTime: "20:35" }, { number: 8, startTime: "20:45", endTime: "22:20" } ]; } function validateSemesterStartDateInput(input) { const value = String(input || "").trim(); if (!value) return "请输入开学日期"; if (!/^\d{4}[-/.]\d{2}[-/.]\d{2}$/.test(value)) return "请输入 YYYY-MM-DD"; const normalized = value.replace(/[/.]/g, "-"); const parts = normalized.split("-"); const year = Number(parts[0]); const month = Number(parts[1]); const day = Number(parts[2]); if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return "请输入有效日期"; const date = new Date(year, month - 1, day); const isValidDate = date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day; return isValidDate ? false : "请输入有效日期"; } window.validateSemesterStartDateInput = validateSemesterStartDateInput; async function selectSemesterStartDate() { const picked = await window.AndroidBridgePromise.showPrompt( "选择开学日期", "请输入开学日期(YYYY-MM-DD)", "", "validateSemesterStartDateInput" ); if (picked === null) return null; const value = String(picked || "").trim().replace(/[/.]/g, "-"); return value || null; } async function runImportFlow() { if (!window.AndroidBridgePromise) { throw new Error("AndroidBridgePromise 不可用,无法进行导入交互。"); } AndroidBridge.showToast("开始自动探测长江大学教务参数..."); // 探测学生 ID 和学期组件 const entryUrl = `${BASE}/eams/courseTableForStd.action?&sf_request_type=ajax`; const entryHtml = await requestText(entryUrl, { method: "GET", headers: { "x-requested-with": "XMLHttpRequest" } }); const params = parseEntryParams(entryHtml); if (!params.studentId || !params.tagId) { await window.AndroidBridgePromise.showAlert( "参数探测失败", "未能识别学生 ID 或学期组件 tagId,请确认已登录后重试。", "确定" ); return; } // 学期选择 const semesterRaw = await requestText(`${BASE}/eams/dataQuery.action?sf_request_type=ajax`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" }, body: `tagId=${encodeURIComponent(params.tagId)}&dataType=semesterCalendar` }); const allSemesters = parseSemesterResponse(semesterRaw); if (allSemesters.length === 0) { throw new Error("学期列表为空,无法继续导入。"); } const recentSemesters = allSemesters.slice(-8); const selectIndex = await window.AndroidBridgePromise.showSingleSelection( "请选择导入学期", JSON.stringify(recentSemesters.map((s) => s.name || s.id)), recentSemesters.length - 1 ); if (selectIndex === null) { AndroidBridge.showToast("已取消导入"); return; } const index = Number.isInteger(Number(selectIndex)) ? Number(selectIndex) : recentSemesters.length - 1; const selectedSemester = recentSemesters[index >= 0 && index < recentSemesters.length ? index : recentSemesters.length - 1]; const semesterStartDate = await selectSemesterStartDate(); if (semesterStartDate === null) { AndroidBridge.showToast("已取消导入"); return; } AndroidBridge.showToast("正在获取课表数据..."); // 拉取并解析课表 const courseHtml = await requestText(`${BASE}/eams/courseTableForStd!courseTable.action?sf_request_type=ajax`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" }, body: [ "ignoreHead=1", "setting.kind=std", "startWeek=", `semester.id=${encodeURIComponent(selectedSemester.id)}`, `ids=${encodeURIComponent(params.studentId)}` ].join("&") }); const courses = parseCoursesFromTaskActivityScript(courseHtml); if (courses.length === 0) { const debugInfo = extractCourseHtmlDebugInfo(courseHtml); await window.AndroidBridgePromise.showAlert( "解析失败", `未能从课表响应中识别到课程。\n响应长度: ${debugInfo.responseLength}\n包含 TaskActivity: ${debugInfo.hasTaskActivity}`, "确定" ); return; } await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses)); await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(getPresetTimeSlots())); await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({ semesterStartDate })); AndroidBridge.showToast(`导入成功,共 ${courses.length} 条课程`); AndroidBridge.notifyTaskCompletion(); } (async function bootstrap() { try { await runImportFlow(); } catch (error) { console.error("导入流程失败:", error); AndroidBridge.showToast(`导入失败:${error && error.message ? error.message : "请检查教务连接"}`); } })(); })();