// 北京邮电大学本科教务管理系统拾光课表适配脚本 // 适配页面:https://jwgl.bupt.edu.cn/jsxsd/xskb/xskb_list.do // 当前版本只解析已打开的“学期理论课表”页面,不主动请求接口。 (function () { function toast(message) { if (window.AndroidBridge && AndroidBridge.showToast) { AndroidBridge.showToast(message); } else { console.log(message); } } async function alertUser(title, message) { if (window.AndroidBridgePromise && window.AndroidBridgePromise.showAlert) { return await window.AndroidBridgePromise.showAlert(title, message, "确定"); } alert(title + "\n" + message); return true; } function normalizeText(text) { return String(text || "") .replace(/\u00a0/g, " ") .replace(/ /gi, " ") .replace(/[0-9]/g, function (ch) { return String.fromCharCode(ch.charCodeAt(0) - 0xFEE0); }) .replace(/[,、]/g, ",") .replace(/[-–—~~至到]/g, "-") .replace(/[()]/g, function (ch) { return ch === "(" ? "(" : ")"; }) .replace(/\s+/g, " ") .trim(); } function findScheduleDocument() { if (document.querySelector("#kbtable")) return document; const frames = Array.from(document.querySelectorAll("iframe")); for (const frame of frames) { try { const frameDoc = frame.contentDocument || frame.contentWindow.document; if (frameDoc && frameDoc.querySelector("#kbtable")) return frameDoc; } catch (e) { // Ignore cross-origin or inaccessible frames. } } return null; } function getTitleText(container, title) { const node = container.querySelector( `font[title="${title}"], span[title="${title}"], div[title="${title}"]` ); return normalizeText(node ? node.textContent : ""); } function extractCourseName(courseDiv) { const clone = courseDiv.cloneNode(true); Array.from(clone.querySelectorAll("font[title], span[title], div[title]")).forEach(function (node) { node.remove(); }); Array.from(clone.querySelectorAll("span")).forEach(function (node) { const text = normalizeText(node.textContent); if (/^[A-Z]$/.test(text) || /^[●★○]+$/.test(text)) node.remove(); }); const holder = document.createElement("div"); holder.innerHTML = clone.innerHTML.replace(//gi, "\n"); const lines = holder.textContent .split(/\n+/) .map(normalizeText) .filter(function (line) { return line && line !== "-" && !/^\(\d+\)$/.test(line); }); return normalizeText((lines[0] || "").replace(/[●★○]/g, "")); } function parseDay(courseDiv, fallbackDay) { const id = courseDiv.getAttribute("id") || ""; const match = id.match(/-(\d)-\d$/); if (match) return parseInt(match[1], 10); return fallbackDay || 0; } function parseWeeks(weekText) { const text = normalizeText(weekText) .replace(/\[[^\]]*\]/g, "") .replace(/\(周\)/g, "") .replace(/周/g, "") .replace(/\s/g, ""); const weeks = new Set(); text.split(/[;,;]/).forEach(function (part) { if (!part) return; const isOdd = /单/.test(part); const isEven = /双/.test(part); const ranges = part.match(/\d+(?:-\d+)?/g) || []; ranges.forEach(function (rangeText) { const range = rangeText.split("-").map(function (value) { return parseInt(value, 10); }); const start = range[0]; const end = range.length > 1 ? range[1] : start; if (!start || !end || start > end) return; for (let week = start; week <= end; week++) { if (isOdd && week % 2 === 0) continue; if (isEven && week % 2 !== 0) continue; weeks.add(week); } }); }); return Array.from(weeks).sort(function (a, b) { return a - b; }); } function parseSections(weekText) { const text = normalizeText(weekText).replace(/\s/g, ""); const match = text.match(/\[([^\]]+)\]/); if (!match) return []; const numbers = match[1].match(/\d+/g) || []; if (numbers.length === 0) return []; const start = parseInt(numbers[0], 10); const end = parseInt(numbers[numbers.length - 1], 10); if (!start || !end || start > end) return []; const sections = []; for (let section = start; section <= end; section++) { sections.push(section); } return sections; } function parseCourseDiv(courseDiv, fallbackDay) { const rawText = normalizeText(courseDiv.textContent); if (!rawText || rawText === " " || rawText.length < 2) return null; const name = extractCourseName(courseDiv); const teacher = getTitleText(courseDiv, "老师") || getTitleText(courseDiv, "教师"); const weekText = getTitleText(courseDiv, "周次(节次)"); const position = getTitleText(courseDiv, "教室") || "未知地点"; const weeks = parseWeeks(weekText); const sections = parseSections(weekText); const day = parseDay(courseDiv, fallbackDay); if (!name || !day || weeks.length === 0 || sections.length === 0) return null; return { name: name, teacher: teacher || "未知教师", position: position, day: day, startSection: sections[0], endSection: sections[sections.length - 1], weeks: weeks }; } function parseCourses(doc) { const table = doc.querySelector("#kbtable"); if (!table) return []; const courses = []; Array.from(table.querySelectorAll("tr")).forEach(function (row) { const cells = Array.from(row.querySelectorAll("td")); cells.forEach(function (cell, index) { const fallbackDay = index + 1; Array.from(cell.querySelectorAll("div.kbcontent")).forEach(function (courseDiv) { if (courseDiv.classList.contains("sykb2")) return; const course = parseCourseDiv(courseDiv, fallbackDay); if (course) courses.push(course); }); }); }); return mergeCourses(courses); } function mergeCourses(courses) { const map = new Map(); courses.forEach(function (course) { const key = [ course.name, course.teacher, course.position, course.day, course.startSection, course.endSection ].join("|"); if (!map.has(key)) { map.set(key, { name: course.name, teacher: course.teacher, position: course.position, day: course.day, startSection: course.startSection, endSection: course.endSection, weeks: course.weeks.slice() }); return; } const existing = map.get(key); existing.weeks = Array.from(new Set(existing.weeks.concat(course.weeks))); }); return Array.from(map.values()) .map(function (course) { course.weeks = course.weeks.sort(function (a, b) { return a - b; }); return course; }) .sort(function (a, b) { return a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name); }); } function parseTimeSlots(doc) { const table = doc.querySelector("#kbtable"); if (!table) return []; const map = new Map(); Array.from(table.querySelectorAll("tr")).forEach(function (row) { const header = row.querySelector("th"); if (!header) return; const text = normalizeText(header.textContent); const match = text.match(/^(\d+).*?(\d{1,2}:\d{2})-(\d{1,2}:\d{2})/); if (!match) return; const number = parseInt(match[1], 10); if (!number || map.has(number)) return; map.set(number, { number: number, startTime: match[2].padStart(5, "0"), endTime: match[3].padStart(5, "0") }); }); return Array.from(map.values()).sort(function (a, b) { return a.number - b.number; }); } async function saveToApp(courses, timeSlots) { const maxWeek = Math.max.apply(null, courses.flatMap(function (course) { return course.weeks; })); const config = { semesterTotalWeeks: Number.isFinite(maxWeek) && maxWeek > 0 ? maxWeek : 20, firstDayOfWeek: 1, defaultClassDuration: 45, defaultBreakDuration: 5 }; if (window.AndroidBridgePromise && window.AndroidBridgePromise.saveCourseConfig) { await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config)); } if (timeSlots.length > 0 && window.AndroidBridgePromise && window.AndroidBridgePromise.savePresetTimeSlots) { await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots)); } if (window.AndroidBridgePromise && window.AndroidBridgePromise.saveImportedCourses) { return await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses)); } console.log("BUPT parsed courses:", JSON.stringify(courses, null, 2)); console.log("BUPT parsed time slots:", JSON.stringify(timeSlots, null, 2)); return true; } async function runImportFlow() { try { const doc = findScheduleDocument(); if (!doc) { await alertUser( "未找到课表", "请不要在教务系统主页直接导入。请先进入“学期理论课表”页面,并等待课表加载完成后再点击导入。" ); return; } const confirmed = await alertUser( "北邮课表导入", "请确认当前不是教务系统主页,而是已经进入“学期理论课表”页面。脚本将直接解析当前页面显示的课表,请确认学期正确且页面已加载完成。" ); if (!confirmed) return; const courses = parseCourses(doc); const timeSlots = parseTimeSlots(doc); if (courses.length === 0) { await alertUser( "未解析到课程", "当前页面没有解析到有效课程。请确认课表页面中存在课程块,或把一段 kbcontent HTML 发给我继续微调。" ); return; } const saved = await saveToApp(courses, timeSlots); if (!saved) { toast("课程保存失败,请重试"); return; } toast(`导入成功:${courses.length} 个课程时段${timeSlots.length ? ",已同步作息时间" : ""}`); if (window.AndroidBridge && AndroidBridge.notifyTaskCompletion) { AndroidBridge.notifyTaskCompletion(); } } catch (error) { console.error("BUPT import failed:", error); await alertUser("导入失败", error && error.message ? error.message : String(error)); } } runImportFlow(); })();