소스 검색

fix(SWJTU): 迁移适配器到新教务系统

weedy233 1 주 전
부모
커밋
481949f2f8
3개의 변경된 파일306개의 추가작업 그리고 326개의 파일을 삭제
  1. 4 4
      resources/SWJTU/adapters.yaml
  2. 0 322
      resources/SWJTU/swjtu_vatuu.js
  3. 302 0
      resources/SWJTU/swjtu_yhxt.js

+ 4 - 4
resources/SWJTU/adapters.yaml

@@ -1,9 +1,9 @@
 # resources/SWJTU/adapters.yaml
 adapters:
   - adapter_id: "SWJTU_01"
-    adapter_name: "西南交通大学VATUU为途教务系统"
+    adapter_name: "西南交通大学扬华学堂"
     category: "BACHELOR_AND_ASSOCIATE"
-    asset_js_path: "swjtu_vatuu.js"
-    import_url: "https://jwc.swjtu.edu.cn/service/login.jsp"
+    asset_js_path: "swjtu_yhxt.js"
+    import_url: "https://yhxt.swjtu.edu.cn/study/teach/course/stu-course-list"
     maintainer: "Weedy233"
-    description: "登录VATUU为途教务系统后,进入本学期选课/课表页面执行导入"
+    description: "登录西南交通大学扬华学堂后,直接点击底栏“执行导入”即可完成导入"

+ 0 - 322
resources/SWJTU/swjtu_vatuu.js

@@ -1,322 +0,0 @@
-// 西南交通大学 VATUU 为途教务系统课表导入脚本
-// 适配页面:/vatuu/CourseAction?setAction=userCourseSchedule&selectTableType=ThisTerm
-
-(() => {
-    const SWJTU_SEMESTER_TOTAL_WEEKS = 19;
-
-    const SWJTU_TIME_SLOTS = [
-        { number: 1, startTime: "08:00", endTime: "08:45" },
-        { number: 2, startTime: "08:50", endTime: "09:35" },
-        { number: 3, startTime: "09:50", endTime: "10:35" },
-        { number: 4, startTime: "10:40", endTime: "11:25" },
-        { number: 5, startTime: "11:30", endTime: "12:15" },
-        { number: 6, startTime: "14:00", endTime: "14:45" },
-        { number: 7, startTime: "14:50", endTime: "15:35" },
-        { number: 8, startTime: "15:40", endTime: "16:25" },
-        { number: 9, startTime: "16:40", endTime: "17:25" },
-        { number: 10, startTime: "17:30", endTime: "18:15" },
-        { number: 11, startTime: "19:30", endTime: "20:15" },
-        { number: 12, startTime: "20:20", endTime: "21:05" },
-        { number: 13, startTime: "21:10", endTime: "21:55" },
-    ];
-
-    const SWJTU_COURSE_CONFIG = {
-        semesterStartDate: null,
-        semesterTotalWeeks: SWJTU_SEMESTER_TOTAL_WEEKS,
-        defaultClassDuration: 45,
-        firstDayOfWeek: 1,
-    };
-
-    function normalizeText(text) {
-        return String(text || "")
-            .replace(/\u00a0/g, " ")
-            .replace(/ /gi, " ")
-            .replace(/[\t\r]+/g, " ")
-            .replace(/ +/g, " ")
-            .trim();
-    }
-
-    function getCellLines(cell) {
-        const cloned = cell.cloneNode(true);
-        cloned.querySelectorAll("br").forEach((br) => {
-            br.replaceWith("\n");
-        });
-        return normalizeText(cloned.textContent)
-            .split(/\n+/)
-            .map(normalizeText)
-            .filter((line) => line && line !== " ");
-    }
-
-    function isCourseHeaderLine(line) {
-        return /^[A-Z]+\d+\s+.+[((].+[))]$/.test(line);
-    }
-
-    function parseCourseHeader(line) {
-        const match = normalizeText(line).match(
-            /^([A-Z]+\d+)\s+(.+?)[((]([^()()]*)[))]$/,
-        );
-        if (!match) return null;
-        return {
-            courseCode: match[1],
-            name: normalizeText(match[2]),
-            teacher: normalizeText(match[3]),
-        };
-    }
-
-    function parseWeeks(weekText) {
-        const weeks = [];
-        const text = normalizeText(weekText)
-            .replace(/第/g, "")
-            .replace(/[[【]/g, "(")
-            .replace(/[\]】]/g, ")");
-        const regex =
-            /(\d+)(?:\s*-\s*(\d+))?\s*周?\s*(?:\((单|双)\)|([单双])周?)?/g;
-        let match = regex.exec(text);
-
-        while (match !== null) {
-            const start = Number(match[1]);
-            const end = match[2] ? Number(match[2]) : start;
-            const parity = match[3] || match[4] || "";
-            if (!Number.isFinite(start) || !Number.isFinite(end) || start > end)
-                continue;
-
-            for (let week = start; week <= end; week++) {
-                if (parity === "单" && week % 2 !== 1) continue;
-                if (parity === "双" && week % 2 !== 0) continue;
-                if (!weeks.includes(week)) weeks.push(week);
-            }
-
-            match = regex.exec(text);
-        }
-
-        return weeks.sort((a, b) => a - b);
-    }
-
-    function parseScheduleLine(line) {
-        const text = normalizeText(line);
-        const match = text.match(
-            /^(.+?周(?:\s*[((][单双][))])?)(?:\s+(.+))?$/,
-        );
-        if (!match) return null;
-
-        const weeks = parseWeeks(match[1]);
-        if (weeks.length === 0) return null;
-
-        return {
-            weeks,
-            position: normalizeText(match[2] || "未指定"),
-        };
-    }
-
-    function parseCoursesFromCell(cell) {
-        const lines = getCellLines(cell);
-        const courses = [];
-
-        for (let i = 0; i < lines.length; i++) {
-            if (!isCourseHeaderLine(lines[i])) continue;
-
-            const header = parseCourseHeader(lines[i]);
-            if (!header?.name) continue;
-
-            let schedule = null;
-            for (let j = i + 1; j < lines.length; j++) {
-                if (isCourseHeaderLine(lines[j])) break;
-                schedule = parseScheduleLine(lines[j]);
-                if (schedule) break;
-            }
-
-            if (schedule) {
-                courses.push({
-                    courseCode: header.courseCode,
-                    name: header.name,
-                    teacher: header.teacher || "未指定",
-                    position: schedule.position || "未指定",
-                    weeks: schedule.weeks,
-                });
-            }
-        }
-
-        return courses;
-    }
-
-    function getCandidateDocuments() {
-        const docs = [document];
-        document.querySelectorAll("iframe").forEach((frame) => {
-            try {
-                if (frame.contentDocument) docs.push(frame.contentDocument);
-            } catch (error) {
-                console.warn("跳过不可访问的 iframe:", error);
-            }
-        });
-        return docs;
-    }
-
-    function findScheduleTable() {
-        for (const doc of getCandidateDocuments()) {
-            const tables = Array.from(doc.querySelectorAll("table"));
-            const table = tables.find((item) => {
-                const text = normalizeText(item.textContent);
-                return (
-                    text.includes("星期一") &&
-                    text.includes("上课时间") &&
-                    text.includes("星期日")
-                );
-            });
-            if (table) return table;
-        }
-        return null;
-    }
-
-    function getTableDiagnostics() {
-        return getCandidateDocuments()
-            .map((doc, index) => {
-                const tableCount = doc.querySelectorAll("table").length;
-                return `文档${index + 1}:${doc.title || "无标题"},URL:${doc.location?.href || "未知"},表格数量:${tableCount}`;
-            })
-            .join(";");
-    }
-
-    async function waitForScheduleTable(timeoutMs = 5000) {
-        const startedAt = Date.now();
-        let table = findScheduleTable();
-        while (!table && Date.now() - startedAt < timeoutMs) {
-            await new Promise((resolve) => setTimeout(resolve, 300));
-            table = findScheduleTable();
-        }
-        return table;
-    }
-
-    function sameCourseForMerge(a, b) {
-        return (
-            a.day === b.day &&
-            a.courseCode === b.courseCode &&
-            a.name === b.name &&
-            a.teacher === b.teacher &&
-            a.position === b.position &&
-            a.weeks.join(",") === b.weeks.join(",")
-        );
-    }
-
-    function mergeContinuousCourses(courseRows) {
-        const merged = [];
-        const sorted = courseRows.slice().sort((a, b) => {
-            if (a.day !== b.day) return a.day - b.day;
-            if (a.startSection !== b.startSection)
-                return a.startSection - b.startSection;
-            return a.name.localeCompare(b.name, "zh-Hans-CN");
-        });
-
-        sorted.forEach((course) => {
-            const last = merged[merged.length - 1];
-            if (
-                last &&
-                sameCourseForMerge(last, course) &&
-                course.startSection === last.endSection + 1
-            ) {
-                last.endSection = course.endSection;
-            } else {
-                merged.push({ ...course });
-            }
-        });
-
-        return merged.map(({ courseCode, ...course }) => course);
-    }
-
-    async function parseScheduleTable() {
-        const table = await waitForScheduleTable();
-        if (!table) {
-            throw new Error(
-                `未找到课表表格,请确认已进入 VATUU 本学期课表页面并等待课表加载完成。${getTableDiagnostics()}`,
-            );
-        }
-
-        const rows = Array.from(table.querySelectorAll("tr")).slice(1);
-        const courseRows = [];
-
-        rows.forEach((row) => {
-            const cells = Array.from(row.querySelectorAll("td"));
-            if (cells.length < 9) return;
-
-            const section = Number(
-                normalizeText(cells[0].textContent).match(/\d+/)?.[0],
-            );
-            if (!Number.isFinite(section)) return;
-
-            for (let day = 1; day <= 7; day++) {
-                const cell = cells[day + 1];
-                parseCoursesFromCell(cell).forEach((course) => {
-                    courseRows.push({
-                        ...course,
-                        day,
-                        startSection: section,
-                        endSection: section,
-                    });
-                });
-            }
-        });
-
-        return mergeContinuousCourses(courseRows);
-    }
-
-    function collectUnscheduledCourses() {
-        const marker = "以下课程由于未安排具体节次时间,无法显示";
-        const text = getCandidateDocuments()
-            .map((doc) => normalizeText(doc.body?.textContent || ""))
-            .find((docText) => docText.includes(marker));
-        if (!text) return [];
-
-        return text
-            .slice(text.indexOf(marker) + marker.length)
-            .split(/(?=[A-Z]+\d+\s+)/)
-            .map(normalizeText)
-            .filter((line) => /^[A-Z]+\d+\s+/.test(line));
-    }
-
-    async function importSwjtuSchedule() {
-        try {
-            window.shiguangBridge.showToast("正在解析西南交通大学 VATUU 课表...");
-
-            const courses = await parseScheduleTable();
-            if (courses.length === 0) {
-                await window.shiguangBridgePromise.showAlert(
-                    "导入失败",
-                    "未解析到课程。请确认当前页面为本学期课表,并选择“全部周次”。",
-                    "确定",
-                );
-                return false;
-            }
-
-            await window.shiguangBridgePromise.saveCourseConfig(
-                JSON.stringify(SWJTU_COURSE_CONFIG),
-            );
-            await window.shiguangBridgePromise.savePresetTimeSlots(
-                JSON.stringify(SWJTU_TIME_SLOTS),
-            );
-            await window.shiguangBridgePromise.saveImportedCourses(
-                JSON.stringify(courses),
-            );
-
-            const unscheduledCourses = collectUnscheduledCourses();
-            if (unscheduledCourses.length > 0) {
-                window.shiguangBridge.showToast(
-                    `成功导入 ${courses.length} 条课程,另有 ${unscheduledCourses.length} 门无节次课程已跳过`,
-                );
-            } else {
-                window.shiguangBridge.showToast(`成功导入 ${courses.length} 条课程`);
-            }
-
-            window.shiguangBridge.notifyTaskCompletion();
-            return true;
-        } catch (error) {
-            console.error("SWJTU VATUU 课表导入失败:", error);
-            await window.shiguangBridgePromise.showAlert(
-                "导入失败",
-                `解析或保存课表失败:${error.message}`,
-                "确定",
-            );
-            return false;
-        }
-    }
-
-    void importSwjtuSchedule();
-})();

+ 302 - 0
resources/SWJTU/swjtu_yhxt.js

@@ -0,0 +1,302 @@
+// 西南交通大学新教务系统课表导入脚本
+// 适配页面:https://yhxt.swjtu.edu.cn/study/teach/course/stu-course-list
+
+(() => {
+  const SWJTU_SCHEDULE_API =
+    "https://yhxt.swjtu.edu.cn/yethan/common/course-schedule/student-course-schedule";
+  const SWJTU_SEMESTER_TOTAL_WEEKS = 19;
+  const MAX_CLASS_TIME_FIELDS = 40;
+
+  const SWJTU_TIME_SLOTS = [
+    { number: 1, startTime: "08:00", endTime: "08:45" },
+    { number: 2, startTime: "08:50", endTime: "09:35" },
+    { number: 3, startTime: "09:50", endTime: "10:35" },
+    { number: 4, startTime: "10:40", endTime: "11:25" },
+    { number: 5, startTime: "11:30", endTime: "12:15" },
+    { number: 6, startTime: "14:00", endTime: "14:45" },
+    { number: 7, startTime: "14:50", endTime: "15:35" },
+    { number: 8, startTime: "15:40", endTime: "16:25" },
+    { number: 9, startTime: "16:40", endTime: "17:25" },
+    { number: 10, startTime: "17:30", endTime: "18:15" },
+    { number: 11, startTime: "19:30", endTime: "20:15" },
+    { number: 12, startTime: "20:20", endTime: "21:05" },
+    { number: 13, startTime: "21:10", endTime: "21:55" },
+  ];
+
+  const SWJTU_COURSE_CONFIG = {
+    semesterStartDate: null,
+    semesterTotalWeeks: SWJTU_SEMESTER_TOTAL_WEEKS,
+    defaultClassDuration: 45,
+    firstDayOfWeek: 1,
+  };
+
+  const DAY_MAP = {
+    一: 1,
+    二: 2,
+    三: 3,
+    四: 4,
+    五: 5,
+    六: 6,
+    日: 7,
+    天: 7,
+  };
+
+  function normalizeText(text) {
+    return String(text || "")
+      .replace(/\u00a0/g, " ")
+      .replace(/&nbsp;/gi, " ")
+      .replace(/[\t\r\n]+/g, " ")
+      .replace(/ +/g, " ")
+      .trim();
+  }
+
+  function cleanValue(value) {
+    const text = normalizeText(value);
+    if (!text || text.toLowerCase() === "null" || text === "无") return "";
+    return text;
+  }
+
+  function getBridge() {
+    return {
+      bridge: window.AndroidBridge || window.shiguangBridge,
+      promise: window.AndroidBridgePromise || window.shiguangBridgePromise,
+    };
+  }
+
+  function showToast(message) {
+    const { bridge } = getBridge();
+    if (bridge?.showToast) {
+      bridge.showToast(message);
+    } else {
+      console.log(message);
+    }
+  }
+
+  async function showAlert(title, message) {
+    const { promise } = getBridge();
+    if (promise?.showAlert) {
+      await promise.showAlert(title, message, "确定");
+    } else {
+      console.warn(`${title}: ${message}`);
+    }
+  }
+
+  function getCookieValue(name) {
+    const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+    const match = document.cookie.match(
+      new RegExp(`(?:^|;\\s*)${escapedName}=([^;]*)`),
+    );
+    return match ? decodeURIComponent(match[1]) : "";
+  }
+
+  function getStorageValue(name) {
+    try {
+      return localStorage.getItem(name) || sessionStorage.getItem(name) || "";
+    } catch (error) {
+      console.warn("读取本地登录态失败:", error);
+      return "";
+    }
+  }
+
+  function getYToken() {
+    const candidates = [
+      getCookieValue("ytoken"),
+      getStorageValue("ytoken"),
+      getStorageValue("YToken"),
+      getStorageValue("token"),
+      getStorageValue("TOKEN"),
+    ];
+    return candidates.map(cleanValue).find(Boolean) || "";
+  }
+
+  async function fetchSemesterSchedule() {
+    const ytoken = getYToken();
+    if (!ytoken) {
+      throw new Error("未找到 ytoken,请确认已登录西南交通大学新教务系统。");
+    }
+
+    const response = await fetch(SWJTU_SCHEDULE_API, {
+      method: "GET",
+      credentials: "include",
+      headers: {
+        Accept: "application/json, text/plain, */*",
+        "Content-Type": "application/json",
+        ytoken,
+      },
+    });
+
+    if (!response.ok) {
+      throw new Error(`课表接口请求失败:HTTP ${response.status}`);
+    }
+
+    const payload = await response.json();
+    if (payload.code !== "00000") {
+      throw new Error(payload.message || `课表接口返回异常:${payload.code}`);
+    }
+
+    if (Array.isArray(payload.data)) return payload.data;
+    if (Array.isArray(payload.data?.list)) return payload.data.list;
+    throw new Error("课表接口返回结构异常:data 不是课程数组。");
+  }
+
+  function parseWeeks(weekText) {
+    const weeks = [];
+    const text = normalizeText(weekText)
+      .replace(/第/g, "")
+      .replace(/[[【]/g, "(")
+      .replace(/[\]】]/g, ")");
+    const parity = text.includes("单") ? "单" : text.includes("双") ? "双" : "";
+    const cleanedText = text
+      .replace(/[单双]/g, "")
+      .replace(/[()()]/g, "")
+      .replace(/周/g, "");
+
+    cleanedText.split(/[,,、;]/).forEach((segment) => {
+      const match = normalizeText(segment).match(
+        /^(\d+)(?:\s*[-~—至]\s*(\d+))?$/,
+      );
+      if (!match) return;
+
+      const start = Number(match[1]);
+      const end = match[2] ? Number(match[2]) : start;
+      if (!Number.isFinite(start) || !Number.isFinite(end) || start > end)
+        return;
+
+      for (let week = start; week <= end; week++) {
+        if (parity === "单" && week % 2 !== 1) continue;
+        if (parity === "双" && week % 2 !== 0) continue;
+        if (!weeks.includes(week)) weeks.push(week);
+      }
+    });
+
+    return weeks.sort((a, b) => a - b);
+  }
+
+  function parseClassTime(classTime) {
+    const text = cleanValue(classTime);
+    if (!text) return [];
+
+    const results = [];
+    const pattern =
+      /((?:第?\d+(?:\s*[-~—至]\s*\d+)?(?:\s*[,,、]\s*第?\d+(?:\s*[-~—至]\s*\d+)?)*\s*周(?:\s*[((]?\s*[单双]\s*[))]?\s*周?)?))\s*(?:星期|周)([一二三四五六日天])\s*第?\s*(\d+)(?:\s*[-~—至]\s*(\d+))?\s*节/g;
+    let match = pattern.exec(text);
+
+    while (match !== null) {
+      const weeks = parseWeeks(match[1]);
+      const day = DAY_MAP[match[2]];
+      const startSection = Number(match[3]);
+      const endSection = match[4] ? Number(match[4]) : startSection;
+
+      if (
+        weeks.length > 0 &&
+        day &&
+        Number.isFinite(startSection) &&
+        Number.isFinite(endSection) &&
+        startSection <= endSection
+      ) {
+        results.push({ weeks, day, startSection, endSection });
+      }
+
+      match = pattern.exec(text);
+    }
+
+    return results;
+  }
+
+  function buildTeacher(item) {
+    const names = [item.staffName, item.staffNameOther]
+      .flatMap((value) => cleanValue(value).split(/[,,、;;]/))
+      .map(cleanValue)
+      .filter(Boolean);
+    return Array.from(new Set(names)).join(",") || "未指定";
+  }
+
+  function buildCourses(apiCourses) {
+    const courses = [];
+    const skipped = [];
+
+    apiCourses.forEach((item) => {
+      const name = cleanValue(item.courseName || item.name || item.kcmc);
+      if (!name) return;
+
+      const teacher = buildTeacher(item);
+      let hasSpecificSchedule = false;
+      let hasAnySchedule = false;
+
+      for (let index = 1; index <= MAX_CLASS_TIME_FIELDS; index++) {
+        const classTime = cleanValue(item[`classTime${index}`]);
+        const classPlace = cleanValue(item[`classPlace${index}`]);
+        if (!classTime) continue;
+
+        hasAnySchedule = true;
+        const parsedTimes = parseClassTime(classTime);
+        if (parsedTimes.length === 0) continue;
+
+        hasSpecificSchedule = true;
+        parsedTimes.forEach((time) => {
+          courses.push({
+            name,
+            teacher,
+            position: classPlace || cleanValue(item.campusName) || "未指定",
+            day: time.day,
+            startSection: time.startSection,
+            endSection: time.endSection,
+            weeks: time.weeks,
+          });
+        });
+      }
+
+      if (hasAnySchedule && !hasSpecificSchedule) {
+        skipped.push(name);
+      }
+    });
+
+    return { courses, skipped };
+  }
+
+  async function saveImportResult(courses, skipped) {
+    const { bridge, promise } = getBridge();
+    if (!promise?.saveImportedCourses) {
+      throw new Error(
+        "未找到保存课表的 Bridge,请在拾光 App 或测试插件中执行。",
+      );
+    }
+
+    await promise.saveCourseConfig(JSON.stringify(SWJTU_COURSE_CONFIG));
+    await promise.savePresetTimeSlots(JSON.stringify(SWJTU_TIME_SLOTS));
+    await promise.saveImportedCourses(JSON.stringify(courses));
+
+    if (skipped.length > 0) {
+      showToast(
+        `成功导入 ${courses.length} 条课程,另有 ${skipped.length} 门无具体节次课程已跳过`,
+      );
+    } else {
+      showToast(`成功导入 ${courses.length} 条课程`);
+    }
+
+    bridge?.notifyTaskCompletion?.();
+  }
+
+  async function importSwjtuSchedule() {
+    try {
+      showToast("正在通过西南交通大学新教务接口获取课表...");
+
+      const apiCourses = await fetchSemesterSchedule();
+      const { courses, skipped } = buildCourses(apiCourses);
+      if (courses.length === 0) {
+        throw new Error(
+          `接口返回 ${apiCourses.length} 门课程,但没有解析到带星期和节次的上课安排。`,
+        );
+      }
+
+      await saveImportResult(courses, skipped);
+      return true;
+    } catch (error) {
+      console.error("SWJTU 新教务课表导入失败:", error);
+      await showAlert("导入失败", `解析或保存课表失败:${error.message}`);
+      return false;
+    }
+  }
+
+  void importSwjtuSchedule();
+})();