Просмотр исходного кода

Merge pull request #569 from XingHeYuZhuan/pending

星河欲转 3 недель назад
Родитель
Сommit
1aec5a5e3e

+ 56 - 1
index/root_index.yaml

@@ -357,6 +357,11 @@ schools:
     name: "济南大学"
     initial: "J"
     resource_folder: "UJN"
+  
+  - id: "JNMC"
+    name: "济宁医学院"
+    initial: "J"
+    resource_folder: "JNMC"
 
   - id: "HBGUHX"
     name: "河北地质大学华信学院"
@@ -822,4 +827,54 @@ schools:
     name: "郑州大学"
     initial: "Z"
     resource_folder: "ZZU"
-    
+    
+  - id: "CCZU"
+    name: "常州大学"
+    initial: "C"
+    resource_folder: "CCZU"    
+    
+  - id: "FIT"
+    name: "福州理工学院"
+    initial: "F"
+    resource_folder: "FIT"
+    
+  - id: "GXCME"
+    name: "广西机电职业技术学院"
+    initial: "G"
+    resource_folder: "GXCME"    
+    
+  - id: "HAUST"
+    name: "河南科技大学"
+    initial: "H"
+    resource_folder: "HAUST"
+    
+  - id: "HIT"
+    name: "哈尔滨工业大学"
+    initial: "H"
+    resource_folder: "HIT"
+    
+  - id: "HUEB"
+    name: "河北经贸大学"
+    initial: "H"
+    resource_folder: "HUEB"
+    
+  - id: "LCUDCC"
+    name: "聊城大学东昌学院"
+    initial: "L"
+    resource_folder: "LCUDCC"
+    
+  - id: "LYU"
+    name: "临沂大学"
+    initial: "L"
+    resource_folder: "LYU"
+
+  - id: "SHXY"
+    name: "绥化学院"
+    initial: "S"
+    resource_folder: "SHXY"    
+
+  - id: "SMXY"
+    name: "三明学院"
+    initial: "S"
+    resource_folder: "SMXY"
+

+ 8 - 0
resources/CCZU/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "CCZU"
+    adapter_name: "常州大学教务管理信息系统"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "cczu.js"
+    import_url: "https://zmvpn.cczu.edu.cn/http/webvpndc2d086cb5b297c15e661687e73c1549/web_jxrw/cx_kb_xsgrkb.aspx"
+    maintainer: "Haooz"
+    description: "常州大学自研.NET ASPX教务系统,需通过VPN访问,进入课表页面后点击导入"

+ 214 - 0
resources/CCZU/cczu.js

@@ -0,0 +1,214 @@
+/**
+ * 常州大学 (CCZU) 课表适配器
+ * 系统: .NET ASPX 自研教务
+ * 课表页面: /web_jxrw/cx_kb_xsgrkb.aspx (iframe)
+ * 数据位置: HTML 表格 #GVxkkb
+ */
+(function () {
+  "use strict";
+
+  function parseWeeks(raw) {
+    if (!raw || raw === "/") return [];
+    const cleaned = raw.replace(/\/+$/, "").trim();
+    if (!cleaned) return [];
+    const weeks = [];
+    const parts = cleaned.split(",");
+    for (const part of parts) {
+      const trimmed = part.trim();
+      if (!trimmed) continue;
+      const rangeMatch = trimmed.match(/^(\d+)\s*[-~]\s*(\d+)$/);
+      if (rangeMatch) {
+        const start = parseInt(rangeMatch[1], 10);
+        const end = parseInt(rangeMatch[2], 10);
+        for (let w = start; w <= end; w++) weeks.push(w);
+      } else {
+        const num = parseInt(trimmed, 10);
+        if (!isNaN(num)) weeks.push(num);
+      }
+    }
+    return [...new Set(weeks)];
+  }
+
+  function parseSection(raw) {
+    if (!raw) return null;
+    const n = parseInt(raw.trim(), 10);
+    return isNaN(n) ? null : n;
+  }
+
+  function parseCourseCell(cellText) {
+    if (!cellText || cellText === "\u00a0" || cellText.trim() === "") return [];
+    const courses = [];
+    const entries = cellText.split("/");
+    for (const entry of entries) {
+      const trimmed = entry.trim();
+      if (!trimmed) continue;
+      const match = trimmed.match(
+        /^(.+?)\s+([A-Za-z0-9\u4e00-\u9fff]+(?:机房|阶)?)\s+([\d,\-~]+(?:,\d+[\-~]\d+)*)\s*,?\s*(.*)$/
+      );
+      if (match) {
+        const courseName = match[1].trim();
+        const room = match[2].trim();
+        const weeksRaw = match[3].trim();
+        const teacher = match[4].trim().replace(/\/+$/, "").trim() || "";
+        courses.push({
+          courseName,
+          room,
+          teacher,
+          weeksRaw,
+          weeks: parseWeeks(weeksRaw),
+        });
+      } else {
+        const simpleMatch = trimmed.match(/^(.+?)\s+([A-Za-z0-9\u4e00-\u9fff]+(?:机房|阶)?)\s*/);
+        if (simpleMatch) {
+          courses.push({
+            courseName: simpleMatch[1].trim(),
+            room: simpleMatch[2].trim(),
+            teacher: "",
+            weeksRaw: "",
+            weeks: [],
+          });
+        }
+      }
+    }
+    return courses;
+  }
+
+  function extractTeacherMap(doc) {
+    const map = {};
+    const target = doc || document;
+    const table = target.getElementById("GVxkall");
+    if (!table) return map;
+    const rows = table.querySelectorAll("tr.dg1-item");
+    for (const row of rows) {
+      const cells = row.querySelectorAll("td");
+      if (cells.length >= 6) {
+        const name = cells[1]?.textContent?.trim();
+        const teacher = cells[5]?.textContent?.trim();
+        if (name && teacher && teacher !== "\u00a0") {
+          map[name] = teacher;
+        }
+      }
+    }
+    return map;
+  }
+
+  function parseSemester(raw) {
+    if (!raw) return null;
+    const m = raw.trim().match(/^(\d{2})-(\d{2})-(\d)$/);
+    if (!m) return null;
+    const startYear = 2000 + parseInt(m[1], 10);
+    const endYear = 2000 + parseInt(m[2], 10);
+    const semester = parseInt(m[3], 10);
+    return { startYear, endYear, semester };
+  }
+
+  function findTable() {
+    let table = document.getElementById("GVxkkb");
+    if (table) return { doc: document, table };
+
+    const iframes = document.querySelectorAll("iframe");
+    for (const iframe of iframes) {
+      try {
+        const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
+        if (!iframeDoc) continue;
+        table = iframeDoc.getElementById("GVxkkb");
+        if (table) return { doc: iframeDoc, table };
+
+        const innerIframes = iframeDoc.querySelectorAll("iframe");
+        for (const inner of innerIframes) {
+          try {
+            const innerDoc = inner.contentDocument || inner.contentWindow.document;
+            if (!innerDoc) continue;
+            table = innerDoc.getElementById("GVxkkb");
+            if (table) return { doc: innerDoc, table };
+          } catch (e) {}
+        }
+      } catch (e) {}
+    }
+    return null;
+  }
+
+  const TimeSlots = [
+    { number: 1, startTime: "08:00", endTime: "08:40" },
+    { number: 2, startTime: "08:45", endTime: "09:25" },
+    { number: 3, startTime: "09:45", endTime: "10:25" },
+    { number: 4, startTime: "10:35", endTime: "11:15" },
+    { number: 5, startTime: "11:20", endTime: "12:00" },
+    { number: 6, startTime: "13:30", endTime: "14:10" },
+    { number: 7, startTime: "14:15", endTime: "14:55" },
+    { number: 8, startTime: "15:15", endTime: "15:55" },
+    { number: 9, startTime: "16:00", endTime: "16:40" },
+    { number: 10, startTime: "18:30", endTime: "19:10" },
+    { number: 11, startTime: "19:15", endTime: "19:55" },
+    { number: 12, startTime: "20:05", endTime: "20:45" }
+  ];
+
+  async function importPresetTimeSlots(timeSlots) {
+    if (timeSlots.length === 0) return;
+    try { await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots)); } catch (e) {}
+  }
+
+  async function runImportFlow() {
+    const result = findTable();
+    if (!result) {
+      window.shiguangBridge.showToast("找不到课表表格,请确保已进入课表页面");
+      return;
+    }
+    const { doc, table } = result;
+
+    const teacherMap = extractTeacherMap(doc);
+    const courses = [];
+    const rows = table.querySelectorAll("tr");
+
+    const semesterSelect = doc.getElementById("DDxq");
+    const semesterRaw = semesterSelect?.value || "";
+    const semester = parseSemester(semesterRaw);
+
+    for (let i = 1; i < rows.length; i++) {
+      const cells = rows[i].querySelectorAll("td");
+      if (cells.length < 8) continue;
+
+      const section = parseSection(cells[0]?.textContent);
+      if (section === null) continue;
+
+      for (let day = 1; day <= 7; day++) {
+        const cellText = cells[day]?.textContent?.trim() || "";
+        if (!cellText || cellText === "\u00a0") continue;
+
+        const parsed = parseCourseCell(cellText);
+        for (const c of parsed) {
+          const mergedTeacher = c.teacher || teacherMap[c.courseName] || "";
+          courses.push({
+            name: c.courseName,
+            teacher: mergedTeacher,
+            position: c.room,
+            day: day,
+            startSection: section,
+            endSection: section + 1,
+            weeks: c.weeks,
+          });
+        }
+      }
+    }
+
+    if (courses.length === 0) {
+      window.shiguangBridge.showToast("未找到课程数据");
+      return;
+    }
+
+    try {
+      await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+      await importPresetTimeSlots(TimeSlots);
+      window.shiguangBridge.showToast(`课程导入成功,共 ${courses.length} 门课程!`);
+      window.shiguangBridge.notifyTaskCompletion();
+    } catch (error) {
+      window.shiguangBridge.showToast("课程保存失败: " + error.message);
+    }
+  }
+
+  if (document.readyState === "loading") {
+    document.addEventListener("DOMContentLoaded", runImportFlow);
+  } else {
+    runImportFlow();
+  }
+})();

+ 8 - 0
resources/FIT/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "FIT"
+    adapter_name: "福州理工学院正方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "fit.js"
+    import_url: "http://oaa.fitedu.net/jwglxt/xtgl/login_slogin.html"
+    maintainer: "Haooz"
+    description: "福州理工学院正方教务V9,进入教务系统后直接点击导入即可"

+ 438 - 0
resources/FIT/fit.js

@@ -0,0 +1,438 @@
+// 福州理工学院(oaa.fitedu.net) 拾光课程表适配脚本
+// 基于正方教务系统接口适配
+// 出现问题请提issues或者提交pr更改
+
+/**
+ * 节次与周次合并去重函数
+ * @param {Array<Object>} courses 原始解析课程数组
+ * @returns {Array<Object>} 合并去重后的课程数组
+ */
+function mergeAndDistinctCourses(courses) {
+    if (!Array.isArray(courses) || courses.length <= 1) return courses;
+
+    // 1. 深拷贝并规范周次数据,过滤无效项
+    const list = courses.map(c => ({
+        ...c,
+        name: c.name || '',
+        teacher: c.teacher || '',
+        position: c.position || '',
+        weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
+    }));
+
+    // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
+    list.sort((a, b) => {
+        return a.name.localeCompare(b.name) ||
+               a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) ||
+               (a.day || 0) - (b.day || 0) ||
+               a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
+               (a.startSection || 0) - (b.startSection || 0);
+    });
+
+    const step1Merged = [];
+    let current = list[0];
+    for (let i = 1; i < list.length; i++) {
+        const next = list[i];
+        const isSameCourseAndWeeks =
+            current.name === next.name &&
+            current.teacher === next.teacher &&
+            current.position === next.position &&
+            current.day === next.day &&
+            current.weeks.join(',') === next.weeks.join(',');
+
+        const isContinuous = current.endSection + 1 === next.startSection;
+        const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
+
+        if (isSameCourseAndWeeks && isContinuous) {
+            current.endSection = next.endSection;
+        } else if (isSameCourseAndWeeks && isDuplicate) {
+            continue;
+        } else {
+            step1Merged.push(current);
+            current = next;
+        }
+    }
+    step1Merged.push(current);
+
+    // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
+    step1Merged.sort((a, b) => {
+        return a.name.localeCompare(b.name) ||
+               a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) ||
+               (a.day || 0) - (b.day || 0) ||
+               (a.startSection || 0) - (b.startSection || 0) ||
+               (a.endSection || 0) - (b.endSection || 0);
+    });
+
+    const step2Merged = [];
+    let cur = step1Merged[0];
+    for (let i = 1; i < step1Merged.length; i++) {
+        const nxt = step1Merged[i];
+        const isSameCourseAndSection =
+            cur.name === nxt.name &&
+            cur.teacher === nxt.teacher &&
+            cur.position === nxt.position &&
+            cur.day === nxt.day &&
+            cur.startSection === nxt.startSection &&
+            cur.endSection === nxt.endSection;
+
+        if (isSameCourseAndSection) {
+            cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
+        } else {
+            step2Merged.push(cur);
+            cur = nxt;
+        }
+    }
+    step2Merged.push(cur);
+
+    return step2Merged;
+}
+
+/**
+ * 解析周次字符串,处理单双周和周次范围
+ */
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+
+    const weekSets = weekStr.split(',');
+    let weeks = [];
+    for (const set of weekSets) {
+        const trimmedSet = set.trim();
+        const rangeMatch = trimmedSet.match(/(\d+)-(\d+)周/);
+        const singleMatch = trimmedSet.match(/^(\d+)周/);
+
+        let start = 0;
+        let end = 0;
+        let processed = false;
+
+        if (rangeMatch) {
+            start = Number(rangeMatch[1]);
+            end = Number(rangeMatch[2]);
+            processed = true;
+        } else if (singleMatch) {
+            start = end = Number(singleMatch[1]);
+            processed = true;
+        }
+
+        if (processed) {
+            const isSingle = trimmedSet.includes('(单)');
+            const isDouble = trimmedSet.includes('(双)');
+
+            for (let w = start; w <= end; w++) {
+                if (isSingle && w % 2 === 0) continue;
+                if (isDouble && w % 2 !== 0) continue;
+                weeks.push(w);
+            }
+        }
+    }
+
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+/**
+ * 解析 API 返回的 JSON 数据
+ */
+function parseJsonData(jsonData) {
+    if (!jsonData || !Array.isArray(jsonData.kbList)) {
+        return [];
+    }
+
+    const rawCourseList = jsonData.kbList;
+    const initialCourseList = [];
+
+    for (const rawCourse of rawCourseList) {
+        if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc ||
+            !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
+            continue;
+        }
+
+        const weeksArray = parseWeeks(rawCourse.zcd);
+        if (weeksArray.length === 0) {
+            continue;
+        }
+
+        const sectionParts = rawCourse.jcs.split('-');
+        const startSection = Number(sectionParts[0]);
+        const endSection = Number(sectionParts[sectionParts.length - 1]);
+        const day = Number(rawCourse.xqj);
+
+        if (isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
+            day < 1 || day > 7 || startSection > endSection) {
+            continue;
+        }
+
+        initialCourseList.push({
+            name: rawCourse.kcmc.trim(),
+            teacher: rawCourse.xm.trim(),
+            position: rawCourse.cdmc.trim(),
+            day: day,
+            startSection: startSection,
+            endSection: endSection,
+            weeks: weeksArray
+        });
+    }
+
+    return mergeAndDistinctCourses(initialCourseList);
+}
+
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "教务系统课表导入",
+        "导入前请确保您已在浏览器中成功登录教务系统",
+        "好的,开始导入"
+    );
+}
+
+/**
+ * 从教务系统获取学年学期选项
+ * 学年:以选中项为中心,取前2年+后2年,共5个选项
+ */
+async function fetchAcademicOptions() {
+    const url = "http://oaa.fitedu.net/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151";
+
+    try {
+        const response = await fetch(url, {
+            method: "GET",
+            credentials: "include"
+        });
+
+        if (!response.ok) return null;
+
+        const htmlText = await response.text();
+        const parser = new DOMParser();
+        const doc = parser.parseFromString(htmlText, "text/html");
+
+        const allYearOptions = Array.from(doc.querySelectorAll("#xnm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({
+                value: opt.value,
+                text: opt.textContent.trim(),
+                selected: opt.selected
+            }));
+
+        const semesterOptions = Array.from(doc.querySelectorAll("#xqm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({
+                value: opt.value,
+                text: opt.textContent.trim(),
+                selected: opt.selected
+            }));
+
+        if (allYearOptions.length === 0 || semesterOptions.length === 0) {
+            return null;
+        }
+
+        const selectedIndex = allYearOptions.findIndex(opt => opt.selected);
+
+        if (selectedIndex === -1) {
+            return {
+                yearOptions: allYearOptions.slice(0, 5),
+                semesterOptions,
+                defaultYearIndex: 0,
+                defaultSemesterIndex: semesterOptions.findIndex(opt => opt.selected) !== -1
+                    ? semesterOptions.findIndex(opt => opt.selected)
+                    : 0
+            };
+        }
+
+        const start = Math.max(0, selectedIndex - 2);
+        const end = Math.min(allYearOptions.length, selectedIndex + 3);
+        const yearOptions = allYearOptions.slice(start, end);
+        const newDefaultIndex = selectedIndex - start;
+        const defaultSemesterIndex = semesterOptions.findIndex(opt => opt.selected);
+
+        return {
+            yearOptions,
+            semesterOptions,
+            defaultYearIndex: newDefaultIndex,
+            defaultSemesterIndex: defaultSemesterIndex !== -1 ? defaultSemesterIndex : 0
+        };
+    } catch (e) {
+        return null;
+    }
+}
+
+/**
+ * 提示用户选择学年和学期
+ */
+async function selectAcademicYearAndSemester() {
+    const optionsData = await fetchAcademicOptions();
+
+    if (!optionsData) {
+        window.shiguangBridge.showToast("从教务系统读取学年学期失败,请确保登录状态。");
+        return null;
+    }
+
+    const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = optionsData;
+
+    const yearTexts = yearOptions.map(item => item.text);
+    const yearIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学年",
+        JSON.stringify(yearTexts),
+        defaultYearIndex
+    );
+
+    if (yearIndex === null || yearIndex === -1) return null;
+    const selectedYearCode = yearOptions[yearIndex].value;
+
+    const semesterTexts = semesterOptions.map(item => item.text);
+    const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesterTexts),
+        defaultSemesterIndex
+    );
+
+    if (semesterIndex === null || semesterIndex === -1) return null;
+    const selectedSemesterCode = semesterOptions[semesterIndex].value;
+
+    return {
+        academicYear: selectedYearCode,
+        semesterCode: selectedSemesterCode
+    };
+}
+
+/**
+ * 获取学期开学日期
+ */
+async function fetchSemesterStartDate(academicYear, semesterCode) {
+    const url = "http://oaa.fitedu.net/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154";
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}`;
+
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: {
+                "accept": "application/json, text/javascript, */*; q=0.01",
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
+                "x-requested-with": "XMLHttpRequest"
+            },
+            body: requestBody,
+            credentials: "include"
+        });
+
+        if (response.ok) {
+            const json = await response.json();
+            if (Array.isArray(json) && json.length > 0) {
+                const firstWeekObj = json.find(item => String(item.zs) === "1" || String(item.zsmc) === "1") || json[0];
+
+                if (firstWeekObj.rq) {
+                    const startDateStr = firstWeekObj.rq.split('/')[0];
+                    if (/^\d{4}-\d{2}-\d{2}$/.test(startDateStr)) {
+                        return startDateStr;
+                    }
+                }
+
+                if (firstWeekObj.zcrq) {
+                    const match = firstWeekObj.zcrq.match(/(\d{4}-\d{2}-\d{2})/);
+                    if (match) return match[1];
+                }
+
+                if (firstWeekObj.ksrq) {
+                    const match = firstWeekObj.ksrq.match(/(\d{4}-\d{2}-\d{2})/);
+                    if (match) return match[1];
+                }
+            }
+        }
+    } catch (e) {
+        // 获取失败不影响主流程
+    }
+    return null;
+}
+
+/**
+ * 请求和解析课程数据
+ */
+async function fetchAndParseCourses(academicYear, semesterCode) {
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=`;
+    const targetUrl = "http://oaa.fitedu.net/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151";
+
+    const [courseResponse, semesterStartDate] = await Promise.all([
+        fetch(targetUrl, {
+            method: "POST",
+            headers: {
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
+            },
+            body: requestBody,
+            credentials: "include"
+        }),
+        fetchSemesterStartDate(academicYear, semesterCode)
+    ]);
+
+    try {
+        if (courseResponse.ok) {
+            const jsonText = await courseResponse.text();
+            const jsonData = JSON.parse(jsonText);
+            if (jsonData && jsonData.kbList) {
+                const parsedCourses = parseJsonData(jsonData);
+                if (parsedCourses.length > 0) {
+                    return {
+                        courses: parsedCourses,
+                        config: {
+                            semesterStartDate: semesterStartDate,
+                            semesterTotalWeeks: 20
+                        }
+                    };
+                }
+            }
+        }
+    } catch (e) {
+        // 请求失败
+    }
+
+    window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
+    return null;
+}
+
+async function saveCourses(parsedCourses) {
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
+        return false;
+    }
+}
+
+async function runImportFlow() {
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("用户取消了导入。");
+        return;
+    }
+
+    const selection = await selectAcademicYearAndSemester();
+    if (!selection) {
+        window.shiguangBridge.showToast("未选择学年学期,导入流程终止。");
+        return;
+    }
+
+    const { academicYear, semesterCode } = selection;
+
+    const result = await fetchAndParseCourses(academicYear, semesterCode);
+    if (result === null) {
+        return;
+    }
+
+    const { courses, config } = result;
+
+    const saveResult = await saveCourses(courses);
+    if (!saveResult) {
+        return;
+    }
+
+    try {
+        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+        let configMsg = "课表配置更新成功!";
+        if (config.semesterStartDate) {
+            configMsg += ` 开学日期:${config.semesterStartDate}`;
+        }
+        window.shiguangBridge.showToast(configMsg);
+    } catch (error) {
+        window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
+    }
+
+    window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();

+ 24 - 23
resources/GDUT/gdut.js

@@ -29,16 +29,18 @@ async function selectSemesterSelection(){
     const now = new Date();
     const currentYear = now.getFullYear();
     const currentMonth = now.getMonth() + 1;
-    const currentSemester = currentMonth >= 9 || currentMonth <= 2 ? 1 : 1;
+    const currentSemester = currentMonth >= 8 || currentMonth <= 2 ? 1 : 2;
+
     const nextSemester = currentSemester === 1 ? 2 : 1;
+    const nextSemesterYear = currentSemester === 1 ? currentYear : currentYear + 1;
 
-    const presetSemeters = [];
+    const presetSemetersId = [];
     const presetSemestersName = [];
 
-    for (let year = currentYear; year >= currentYear - 3; year--){
+    for (let year = nextSemesterYear; year >= nextSemesterYear - 3; year--){
         for (let semester = nextSemester; semester >= 1; semester--){
+            presetSemetersId.push(`${year}0${semester}`);
             const semesterName = `${year}-${year + 1}学年 ${semester === 1 ? "秋季" : "春季"}(第${semester}学期)`;
-            presetSemeters.push({ name: semesterName, semesterId: `${year}0${semester}` });
             presetSemestersName.push(semesterName);
         }
     }
@@ -49,10 +51,9 @@ async function selectSemesterSelection(){
             JSON.stringify(presetSemestersName),
             2
         );
-        if (selectedIndex !== null && selectedIndex >= 0 && selectedIndex < presetSemeters.length) {
-            const selecedSemester = presetSemeters[selectedIndex];
-            console.log("用户选择了: " + selecedSemester.name + " (索引: " + selectedIndex + ")");
-            return selecedSemester;
+        if (selectedIndex !== null && selectedIndex >= 0 && selectedIndex < presetSemetersId.length) {
+            console.log("用户选择了: " + presetSemestersName[selectedIndex] + " (索引: " + selectedIndex + ")");
+            return presetSemetersId[selectedIndex];
         } else {
             console.log("用户取消了选择。");
             return null;
@@ -190,6 +191,7 @@ async function fetchCourses(semesterId){
 
     } catch (error) {
         console.error('添加课程表失败:', error);
+        window.shiguangBridge.showToast(`添加课程失败: ${error.message}`);
         return null;
     }
 }
@@ -285,7 +287,7 @@ async function setPresetTimeSlots() {
             console.log("预设时间段导入成功!");
         } else {
             console.log("预设时间段导入未成功,结果:" + result);
-            window.shiguangBridge.showToast("测试时间段导入失败,请查看日志。");
+            window.shiguangBridge.showToast("预设时间段导入失败,请查看日志。");
         }
     } catch (error) {
         console.error("导入时间段时发生错误:", error);
@@ -304,11 +306,11 @@ async function saveConfig(config) {
             console.log("课表配置导入成功!");
         } else {
             console.log("课表配置导入未成功,结果:" + result);
-            window.shiguangBridge.showToast("测试配置导入失败,请查看日志。");
+            window.shiguangBridge.showToast("课表配置导入失败,请查看日志。");
         }
     } catch (error) {
-        console.error("导入配置时发生错误:", error);
-        window.shiguangBridge.showToast("导入配置失败: " + error.message);
+        console.error("导入课表配置时发生错误:", error);
+        window.shiguangBridge.showToast("导入课表配置失败: " + error.message);
     }
 }
 
@@ -323,14 +325,21 @@ async function runImportFlow() {
         return; // 用户取消,立即退出函数
     }
 
-    const semester = await selectSemesterSelection();
+    const semesterId = await selectSemesterSelection();
 
-    if (!semester) {
+    if (!semesterId) {
         console.log("用户取消了学期选择,停止后续执行。");
         return; // 用户取消,立即退出函数
     }
 
-    const startDate = await fetchStartDate(semester.semesterId);
+    const startDate = await fetchStartDate(semesterId);
+
+    const courses = await fetchCourses(semesterId);
+
+    if (!courses) {
+        console.log(`未能获取课程数据,停止后续执行。`);
+        return; // 获取课程失败,立即退出函数
+    }
 
     const config = {
         semesterStartDate: startDate.toISOString().split('T')[0], // 转换为 YYYY-MM-DD 格式
@@ -341,14 +350,6 @@ async function runImportFlow() {
     }
 
     await saveConfig(config);
-
-    const courses = await fetchCourses(semester.semesterId);
-
-    if (!courses) {
-        console.log(`未能获取课程数据,停止后续执行。`);
-        return; // 获取课程失败,立即退出函数
-    }
-
     await saveCourses(courses);
     await setPresetTimeSlots();
 

+ 8 - 0
resources/GXCME/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "GXCME"
+    adapter_name: "广西机电职业技术学院正方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "gxcme.js"
+    import_url: "https://jwxt.gxcme.edu.cn/jwglxt/xtgl/login_slogin.html"
+    maintainer: "Haooz"
+    description: "广西机电职业技术学院正方教务V9(Mobile版API),进入课表页面后点击导入"

+ 351 - 0
resources/GXCME/gxcme.js

@@ -0,0 +1,351 @@
+// 广西机电职业技术学院(gxcme.edu.cn) 拾光课程表适配脚本
+// 基于正方教务系统Mobile版接口适配
+
+function mergeAndDistinctCourses(courses) {
+    if (!Array.isArray(courses) || courses.length <= 1) return courses;
+
+    const list = courses.map(c => ({
+        ...c,
+        name: c.name || '',
+        teacher: c.teacher || '',
+        position: c.position || '',
+        weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
+    }));
+
+    list.sort((a, b) => {
+        return a.name.localeCompare(b.name) ||
+               a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) ||
+               (a.day || 0) - (b.day || 0) ||
+               a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
+               (a.startSection || 0) - (b.startSection || 0);
+    });
+
+    const step1Merged = [];
+    let current = list[0];
+    for (let i = 1; i < list.length; i++) {
+        const next = list[i];
+        const isSameCourseAndWeeks =
+            current.name === next.name &&
+            current.teacher === next.teacher &&
+            current.position === next.position &&
+            current.day === next.day &&
+            current.weeks.join(',') === next.weeks.join(',');
+        const isContinuous = current.endSection + 1 === next.startSection;
+        const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
+        if (isSameCourseAndWeeks && isContinuous) {
+            current.endSection = next.endSection;
+        } else if (isSameCourseAndWeeks && isDuplicate) {
+            continue;
+        } else {
+            step1Merged.push(current);
+            current = next;
+        }
+    }
+    step1Merged.push(current);
+
+    step1Merged.sort((a, b) => {
+        return a.name.localeCompare(b.name) ||
+               a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) ||
+               (a.day || 0) - (b.day || 0) ||
+               (a.startSection || 0) - (b.startSection || 0) ||
+               (a.endSection || 0) - (b.endSection || 0);
+    });
+
+    const step2Merged = [];
+    let cur = step1Merged[0];
+    for (let i = 1; i < step1Merged.length; i++) {
+        const nxt = step1Merged[i];
+        const isSameCourseAndSection =
+            cur.name === nxt.name &&
+            cur.teacher === nxt.teacher &&
+            cur.position === nxt.position &&
+            cur.day === nxt.day &&
+            cur.startSection === nxt.startSection &&
+            cur.endSection === nxt.endSection;
+        if (isSameCourseAndSection) {
+            cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
+        } else {
+            step2Merged.push(cur);
+            cur = nxt;
+        }
+    }
+    step2Merged.push(cur);
+
+    return step2Merged;
+}
+
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+    const weekSets = weekStr.split(',');
+    let weeks = [];
+    for (const set of weekSets) {
+        const trimmedSet = set.trim();
+        const rangeMatch = trimmedSet.match(/(\d+)-(\d+)周/);
+        const singleMatch = trimmedSet.match(/^(\d+)周/);
+        let start = 0, end = 0, processed = false;
+        if (rangeMatch) {
+            start = Number(rangeMatch[1]);
+            end = Number(rangeMatch[2]);
+            processed = true;
+        } else if (singleMatch) {
+            start = end = Number(singleMatch[1]);
+            processed = true;
+        }
+        if (processed) {
+            const isSingle = trimmedSet.includes('(单)');
+            const isDouble = trimmedSet.includes('(双)');
+            for (let w = start; w <= end; w++) {
+                if (isSingle && w % 2 === 0) continue;
+                if (isDouble && w % 2 !== 0) continue;
+                weeks.push(w);
+            }
+        }
+    }
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+function parseJsonData(jsonData) {
+    if (!jsonData || !Array.isArray(jsonData.kbList)) return [];
+    const initialCourseList = [];
+    for (const rawCourse of jsonData.kbList) {
+        if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc ||
+            !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) continue;
+        const weeksArray = parseWeeks(rawCourse.zcd);
+        if (weeksArray.length === 0) continue;
+        const sectionParts = rawCourse.jcs.split('-');
+        const startSection = Number(sectionParts[0]);
+        const endSection = Number(sectionParts[sectionParts.length - 1]);
+        const day = Number(rawCourse.xqj);
+        if (isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
+            day < 1 || day > 7 || startSection > endSection) continue;
+        initialCourseList.push({
+            name: rawCourse.kcmc.trim(),
+            teacher: rawCourse.xm.trim(),
+            position: rawCourse.cdmc.trim(),
+            day: day,
+            startSection: startSection,
+            endSection: endSection,
+            weeks: weeksArray
+        });
+    }
+    return mergeAndDistinctCourses(initialCourseList);
+}
+
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "教务系统课表导入",
+        "导入前请确保您已在浏览器中成功登录教务系统",
+        "好的,开始导入"
+    );
+}
+
+async function fetchAcademicOptions() {
+    const url = "/jwglxt/kbcx/xskbcxZccx_cxXskbcxIndex.html?gnmkdm=N2154&layout=default";
+    try {
+        const response = await fetch(url, { method: "GET", credentials: "include" });
+        if (!response.ok) return null;
+        const htmlText = await response.text();
+        const parser = new DOMParser();
+        const doc = parser.parseFromString(htmlText, "text/html");
+
+        const allYearOptions = Array.from(doc.querySelectorAll("#xnm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({
+                value: opt.value,
+                text: opt.textContent.trim(),
+                selected: opt.hasAttribute("selected")
+            }));
+
+        const semesterOptions = Array.from(doc.querySelectorAll("#xqm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({
+                value: opt.value,
+                text: opt.textContent.trim(),
+                selected: opt.hasAttribute("selected")
+            }));
+
+        if (allYearOptions.length === 0 || semesterOptions.length === 0) return null;
+
+        const selectedIndex = allYearOptions.findIndex(opt => opt.selected);
+        if (selectedIndex === -1) {
+            return {
+                yearOptions: allYearOptions.slice(0, 5),
+                semesterOptions,
+                defaultYearIndex: 0,
+                defaultSemesterIndex: semesterOptions.findIndex(opt => opt.selected) !== -1
+                    ? semesterOptions.findIndex(opt => opt.selected) : 0
+            };
+        }
+
+        const start = Math.max(0, selectedIndex - 2);
+        const end = Math.min(allYearOptions.length, selectedIndex + 3);
+        return {
+            yearOptions: allYearOptions.slice(start, end),
+            semesterOptions,
+            defaultYearIndex: selectedIndex - start,
+            defaultSemesterIndex: semesterOptions.findIndex(opt => opt.selected) !== -1
+                ? semesterOptions.findIndex(opt => opt.selected) : 0
+        };
+    } catch (e) {
+        return null;
+    }
+}
+
+async function selectAcademicYearAndSemester() {
+    const optionsData = await fetchAcademicOptions();
+    if (!optionsData) {
+        window.shiguangBridge.showToast("从教务系统读取学年学期失败,请确保登录状态。");
+        return null;
+    }
+    const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = optionsData;
+
+    const yearIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学年",
+        JSON.stringify(yearOptions.map(item => item.text)),
+        defaultYearIndex
+    );
+    if (yearIndex === null || yearIndex === -1) return null;
+
+    const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesterOptions.map(item => item.text)),
+        defaultSemesterIndex
+    );
+    if (semesterIndex === null || semesterIndex === -1) return null;
+
+    return {
+        academicYear: yearOptions[yearIndex].value,
+        semesterCode: semesterOptions[semesterIndex].value
+    };
+}
+
+async function fetchSemesterStartDate(academicYear, semesterCode) {
+    const url = "/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154";
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: {
+                "accept": "application/json, text/javascript, */*; q=0.01",
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
+                "x-requested-with": "XMLHttpRequest"
+            },
+            body: `xnm=${academicYear}&xqm=${semesterCode}`,
+            credentials: "include"
+        });
+        if (response.ok) {
+            const json = await response.json();
+            if (Array.isArray(json) && json.length > 0) {
+                const firstWeekObj = json.find(item => String(item.zs) === "1" || String(item.zsmc) === "1") || json[0];
+                if (firstWeekObj.rq) {
+                    const startDateStr = firstWeekObj.rq.split('/')[0];
+                    if (/^\d{4}-\d{2}-\d{2}$/.test(startDateStr)) return startDateStr;
+                }
+                if (firstWeekObj.zcrq) {
+                    const match = firstWeekObj.zcrq.match(/(\d{4}-\d{2}-\d{2})/);
+                    if (match) return match[1];
+                }
+            }
+        }
+    } catch (e) {}
+    return null;
+}
+
+async function fetchAndParseCourses(academicYear, semesterCode) {
+    const targetUrl = "/jwglxt/kbcx/xskbcxMobile_cxXsKb.html?gnmkdm=N2154";
+    const [courseResponse, semesterStartDate] = await Promise.all([
+        fetch(targetUrl, {
+            method: "POST",
+            headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" },
+            body: `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck`,
+            credentials: "include"
+        }),
+        fetchSemesterStartDate(academicYear, semesterCode)
+    ]);
+
+    try {
+        if (courseResponse.ok) {
+            const jsonData = await courseResponse.json();
+            if (jsonData && jsonData.kbList) {
+                const parsedCourses = parseJsonData(jsonData);
+                if (parsedCourses.length > 0) {
+                    return {
+                        courses: parsedCourses,
+                        config: {
+                            semesterStartDate: semesterStartDate,
+                            semesterTotalWeeks: 20
+                        }
+                    };
+                }
+            }
+        }
+    } catch (e) {}
+    window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
+    return null;
+}
+
+async function saveCourses(parsedCourses) {
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast("课程保存失败: " + error.message);
+        return false;
+    }
+}
+
+const TimeSlots = [
+    { number: 1, startTime: "08:10", endTime: "08:50" },
+    { number: 2, startTime: "09:00", endTime: "09:40" },
+    { number: 3, startTime: "09:40", endTime: "10:20" },
+    { number: 4, startTime: "10:30", endTime: "11:10" },
+    { number: 5, startTime: "11:20", endTime: "12:00" },
+    { number: 6, startTime: "12:10", endTime: "12:50" },
+    { number: 7, startTime: "14:40", endTime: "15:20" },
+    { number: 8, startTime: "15:30", endTime: "16:10" },
+    { number: 9, startTime: "16:10", endTime: "16:50" },
+    { number: 10, startTime: "17:00", endTime: "17:40" },
+    { number: 11, startTime: "17:50", endTime: "18:30" },
+    { number: 12, startTime: "19:30", endTime: "20:10" },
+    { number: 13, startTime: "20:20", endTime: "21:00" },
+    { number: 14, startTime: "21:00", endTime: "21:40" }
+];
+
+async function importPresetTimeSlots(timeSlots) {
+    if (timeSlots.length === 0) return;
+    try { await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots)); } catch (e) {}
+}
+
+async function runImportFlow() {
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("用户取消了导入。");
+        return;
+    }
+
+    const selection = await selectAcademicYearAndSemester();
+    if (!selection) {
+        window.shiguangBridge.showToast("未选择学年学期,导入流程终止。");
+        return;
+    }
+
+    const { academicYear, semesterCode } = selection;
+    const result = await fetchAndParseCourses(academicYear, semesterCode);
+    if (result === null) return;
+
+    const { courses, config } = result;
+    const saveResult = await saveCourses(courses);
+    if (!saveResult) return;
+
+    try {
+        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+    } catch (error) {}
+
+    await importPresetTimeSlots(TimeSlots);
+
+    window.shiguangBridge.showToast("课程导入成功,共导入 " + courses.length + " 门课程!");
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();

+ 8 - 0
resources/HAUST/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "HAUST"
+    adapter_name: "河南科技大学树维教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "haust.js"
+    import_url: "https://vpn.haust.edu.cn"
+    maintainer: "Haooz"
+    description: "河南科技大学新版树维教务,需通过VPN访问,登录后进入课表页面点击导入"

+ 349 - 0
resources/HAUST/haust.js

@@ -0,0 +1,349 @@
+// resources/HAUST/haust.js
+// 河南科技大学 - 新版树维教务
+
+async function promptUserToStart() {
+    try {
+        var confirmed = await window.shiguangBridgePromise.showAlert(
+            "教务系统课表导入",
+            "请按以下步骤操作:\n\n1. 先在浏览器中登录VPN\n2. 进入教务系统\n3. 点击【我的课表】\n4. 确保课表已完整显示\n5. 然后点击下方按钮开始导入",
+            "我已进入课表页面,开始导入"
+        );
+        return confirmed === true;
+    } catch (error) {
+        console.error("显示弹窗出错:", error);
+        return false;
+    }
+}
+
+async function getSemesterList() {
+    try {
+        var response = await fetch("/eams/dataQuery.action", {
+            method: "POST",
+            headers: { "Content-Type": "application/x-www-form-urlencoded" },
+            body: "dataType=semesterCalendar&tagId=semesterBar&empty=true",
+            credentials: "include"
+        });
+
+        if (!response.ok) {
+            throw new Error("请求失败: " + response.status);
+        }
+
+        var text = await response.text();
+        var data = eval("(" + text + ")");
+
+        if (!data || !data.semesters) {
+            throw new Error("未找到学期数据");
+        }
+
+        var allSemesters = [];
+        for (var key in data.semesters) {
+            if (key.startsWith("y") && Array.isArray(data.semesters[key])) {
+                for (var i = 0; i < data.semesters[key].length; i++) {
+                    allSemesters.push(data.semesters[key][i]);
+                }
+            }
+        }
+
+        if (allSemesters.length === 0) {
+            throw new Error("学期列表为空");
+        }
+
+        allSemesters.sort(function(a, b) {
+            var yearA = a.schoolYear.split("-")[0];
+            var yearB = b.schoolYear.split("-")[0];
+            if (yearB !== yearA) return parseInt(yearB) - parseInt(yearA);
+            return parseInt(b.name) - parseInt(a.name);
+        });
+
+        var semesterTexts = allSemesters.map(function(sem) {
+            return sem.schoolYear + "学年第" + sem.name + "学期";
+        });
+
+        var selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+            "选择学期",
+            JSON.stringify(semesterTexts),
+            0
+        );
+
+        if (selectedIndex !== null && selectedIndex >= 0) {
+            window.shiguangBridge.showToast("已选择: " + semesterTexts[selectedIndex]);
+            return allSemesters[selectedIndex];
+        }
+
+        return null;
+    } catch (error) {
+        console.error("获取学期出错:", error);
+        window.shiguangBridge.showToast("获取学期出错: " + error.message);
+        return null;
+    }
+}
+
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+    var cleaned = weekStr.replace(/周/g, "").trim();
+    if (!cleaned) return [];
+
+    var weeks = [];
+    var parts = cleaned.split(",");
+
+    for (var p = 0; p < parts.length; p++) {
+        var trimmed = parts[p].trim();
+        if (!trimmed) continue;
+
+        var isOdd = trimmed.charAt(0) === "单";
+        var isEven = trimmed.charAt(0) === "双";
+        var rangeStr = isOdd || isEven ? trimmed.substring(1) : trimmed;
+
+        var ranges = rangeStr.split(/\s+/);
+        for (var r = 0; r < ranges.length; r++) {
+            var range = ranges[r];
+            var rangeMatch = range.match(/^(\d+)\s*[-~]\s*(\d+)$/);
+            if (rangeMatch) {
+                var start = parseInt(rangeMatch[1], 10);
+                var end = parseInt(rangeMatch[2], 10);
+                for (var w = start; w <= end; w++) {
+                    if (isOdd && w % 2 === 0) continue;
+                    if (isEven && w % 2 === 1) continue;
+                    weeks.push(w);
+                }
+            } else {
+                var num = parseInt(range, 10);
+                if (!isNaN(num)) {
+                    if (isOdd && num % 2 === 0) continue;
+                    if (isEven && num % 2 === 1) continue;
+                    weeks.push(num);
+                }
+            }
+        }
+    }
+
+    weeks.sort(function(a, b) { return a - b; });
+    var unique = [];
+    var seen = {};
+    for (var i = 0; i < weeks.length; i++) {
+        if (!seen[weeks[i]]) {
+            unique.push(weeks[i]);
+            seen[weeks[i]] = true;
+        }
+    }
+    return unique;
+}
+
+function parsePeriod(periodStr) {
+    if (!periodStr) return null;
+    var match = periodStr.match(/(\d+)\s*[-~]\s*(\d+)/);
+    if (match) {
+        return { start: parseInt(match[1], 10), end: parseInt(match[2], 10) };
+    }
+    return null;
+}
+
+function stripHtml(html) {
+    if (!html) return "";
+    return html.replace(/<[^>]+>/g, "").trim();
+}
+
+function parseTitle(title) {
+    if (!title) return [];
+    var courses = [];
+    var cleanTitle = stripHtml(title);
+
+    // 格式: "课程名(课程号) (教师);;;(周次,节次,教室)"
+    // 先提取课程名、课程号、教师
+    var nameMatch = cleanTitle.match(/^(.+?)\(([^)]+)\)\s*\(([^)]+)\)/);
+    if (!nameMatch) return [];
+
+    var courseName = nameMatch[1].trim();
+    var teacher = nameMatch[3].trim();
+
+    // 提取周次、节次、教室 - 找到含"周"的括号内容,手动提取到最后一个")"
+    var detailStart = cleanTitle.search(/\(\d+[-~]\d+周/);
+    if (detailStart === -1) return [];
+
+    var lastParen = cleanTitle.lastIndexOf(")");
+    if (lastParen <= detailStart) return [];
+
+    var details = cleanTitle.substring(detailStart + 1, lastParen);
+    var parts = details.split(",");
+
+    var weeksStr = parts.length >= 1 ? parts[0].trim() : "";
+    var periodStr = parts.length >= 2 ? parts[1].trim() : "";
+    var room = parts.length >= 3 ? parts.slice(2).join(",").trim() : "";
+
+    var weeks = parseWeeks(weeksStr);
+    var period = parsePeriod(periodStr);
+
+    if (weeks.length > 0 && period) {
+        courses.push({
+            courseName: courseName,
+            teacher: teacher,
+            weeks: weeks,
+            startSection: period.start,
+            endSection: period.end,
+            room: room
+        });
+    }
+
+    return courses;
+}
+
+function tryFindCourseTable() {
+    // 方法1: 当前页面直接查找
+    var cells = document.querySelectorAll("td[title]");
+    if (cells.length === 0) cells = document.querySelectorAll("td[id^='TD']");
+    if (cells.length > 0) {
+        return { doc: document, cells: cells, source: "current" };
+    }
+
+    // 方法2: iframe
+    var iframes = document.querySelectorAll("iframe");
+    for (var i = 0; i < iframes.length; i++) {
+        try {
+            var iframeDoc = iframes[i].contentDocument || iframes[i].contentWindow.document;
+            if (!iframeDoc) continue;
+
+            cells = iframeDoc.querySelectorAll("td[title]");
+            if (cells.length === 0) cells = iframeDoc.querySelectorAll("td[id^='TD']");
+            if (cells.length > 0) {
+                return { doc: iframeDoc, cells: cells, source: "iframe" };
+            }
+
+            // 方法3: iframe的iframe
+            var innerIframes = iframeDoc.querySelectorAll("iframe");
+            for (var j = 0; j < innerIframes.length; j++) {
+                try {
+                    var innerDoc = innerIframes[j].contentDocument || innerIframes[j].contentWindow.document;
+                    if (!innerDoc) continue;
+
+                    cells = innerDoc.querySelectorAll("td[title]");
+                    if (cells.length === 0) cells = innerDoc.querySelectorAll("td[id^='TD']");
+                    if (cells.length > 0) {
+                        return { doc: innerDoc, cells: cells, source: "inner-iframe" };
+                    }
+                } catch (e) {}
+            }
+        } catch (e) {}
+    }
+
+    return null;
+}
+
+function parseCoursesFromTable(tableData) {
+    var allCourses = [];
+    var seenKeys = {};
+    var cells = tableData.cells;
+
+    for (var i = 0; i < cells.length; i++) {
+        var cell = cells[i];
+        var title = cell.getAttribute("title");
+        if (!title || !title.trim()) continue;
+
+        var row = cell.parentElement;
+        if (!row) continue;
+
+        var rowCells = Array.from(row.cells);
+        var colIndex = rowCells.indexOf(cell);
+
+        var day = colIndex;
+        if (day < 1 || day > 7) continue;
+
+        var parsed = parseTitle(title);
+
+        for (var j = 0; j < parsed.length; j++) {
+            var course = parsed[j];
+            if (course.weeks.length === 0 || course.startSection === 0) continue;
+
+            var key = course.courseName + "_" + day + "_" + course.startSection + "_" + course.weeks.join(",");
+            if (seenKeys[key]) continue;
+            seenKeys[key] = true;
+
+            allCourses.push({
+                name: course.courseName,
+                teacher: course.teacher,
+                position: course.room,
+                day: day,
+                startSection: course.startSection,
+                endSection: course.endSection,
+                weeks: course.weeks
+            });
+        }
+    }
+
+    return allCourses;
+}
+
+async function saveCourses(courses) {
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast("课程保存失败: " + error.message);
+        return false;
+    }
+}
+
+async function runImportFlow() {
+    window.shiguangBridge.showToast("开始导入...");
+
+    var alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("已取消导入");
+        return;
+    }
+
+    window.shiguangBridge.showToast("正在查找课表...");
+
+    var tableData = tryFindCourseTable();
+    if (!tableData) {
+        window.shiguangBridge.showToast("未找到课表,请确保已进入【我的课表】页面");
+        return;
+    }
+
+    window.shiguangBridge.showToast("找到课表 (" + tableData.source + "),共 " + tableData.cells.length + " 个单元格");
+
+    // 调试:输出前几个单元格信息
+    var debugInfo = [];
+    for (var d = 0; d < Math.min(5, tableData.cells.length); d++) {
+        var dc = tableData.cells[d];
+        var dt = dc.getAttribute("title") || "";
+        var dr = dc.parentElement;
+        var dci = -1;
+        if (dr) {
+            var drc = Array.from(dr.cells);
+            dci = drc.indexOf(dc);
+        }
+        debugInfo.push(dc.id + "|col:" + dci + "|day:" + dci + "|title:" + dt.substring(0, 80));
+    }
+    console.log("HAUST调试: " + debugInfo.join(" || "));
+
+    var courses = parseCoursesFromTable(tableData);
+    if (courses.length === 0) {
+        // 输出解析失败的调试信息
+        var failDebug = [];
+        for (var f = 0; f < Math.min(3, tableData.cells.length); f++) {
+            var fc = tableData.cells[f];
+            var ft = fc.getAttribute("title") || "";
+            var fr = fc.parentElement;
+            var fci = -1;
+            if (fr) {
+                var frc = Array.from(fr.cells);
+                fci = frc.indexOf(fc);
+            }
+            var parsed = parseTitle(ft);
+            failDebug.push("col:" + fci + "|parsed:" + parsed.length + "|raw:" + ft.substring(0, 60));
+        }
+        window.shiguangBridge.showToast("解析失败: " + failDebug[0]);
+    }
+    if (courses.length === 0) {
+        window.shiguangBridge.showToast("未解析到课程,请检查课表是否完整显示");
+        return;
+    }
+
+    var saveResult = await saveCourses(courses);
+    if (!saveResult) return;
+
+    window.shiguangBridge.showToast("导入成功!共 " + courses.length + " 门课程");
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();

+ 8 - 0
resources/HIT/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "HIT"
+    adapter_name: "哈尔滨工业大学研究生系统"
+    category: "POSTGRADUATE"
+    asset_js_path: "hit.js"
+    import_url: "http://yjsgl-hit-edu-cn.ivpn.hit.edu.cn:1080/xs/index"
+    maintainer: "Haooz"
+    description: "哈尔滨工业大学研究生教育管理系统,需通过VPN访问,登录后点击导入"

+ 248 - 0
resources/HIT/hit.js

@@ -0,0 +1,248 @@
+// 哈尔滨工业大学(hit.edu.cn) 研究生课表适配脚本
+// 研究生教育管理系统
+
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+    const cleaned = weekStr.replace(/周/g, '').trim();
+    if (!cleaned) return [];
+    const weeks = [];
+    const parts = cleaned.split(/[,,]/);
+    for (const part of parts) {
+        const trimmed = part.trim();
+        if (!trimmed) continue;
+        const isOdd = trimmed.startsWith('单');
+        const isEven = trimmed.startsWith('双');
+        const rangeStr = isOdd || isEven ? trimmed.substring(1) : trimmed;
+        const rangeMatch = rangeStr.match(/^(\d+)\s*[-~]\s*(\d+)$/);
+        const singleMatch = rangeStr.match(/^(\d+)$/);
+        if (rangeMatch) {
+            const start = parseInt(rangeMatch[1], 10);
+            const end = parseInt(rangeMatch[2], 10);
+            for (let w = start; w <= end; w++) {
+                if (isOdd && w % 2 === 0) continue;
+                if (isEven && w % 2 !== 0) continue;
+                weeks.push(w);
+            }
+        } else if (singleMatch) {
+            const num = parseInt(singleMatch[1], 10);
+            if (isOdd && num % 2 === 0) continue;
+            if (isEven && num % 2 !== 0) continue;
+            weeks.push(num);
+        }
+    }
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+function parseCourseCell(cellStr, day, sections) {
+    if (!cellStr || cellStr === 'null') return [];
+    const courses = [];
+    const entries = cellStr.split('<br/>');
+    for (const entry of entries) {
+        const trimmed = entry.trim();
+        if (!trimmed) continue;
+        // 格式: 课程名◇教师[周次]教室[节次]节
+        // 先找◇分割课程名和教师
+        const teacherSplit = trimmed.split('\u25C7');
+        if (teacherSplit.length < 2) {
+            console.log("HIT调试: 无◇分隔, text=" + trimmed.substring(0, 40));
+            continue;
+        }
+        const courseName = teacherSplit[0].trim();
+        const rest = teacherSplit.slice(1).join('\u25C7').trim();
+
+        // 从rest中提取: 教师[周次]教室[节次]节
+        // 找第一个[之前的是教师
+        const firstBracket = rest.indexOf('[');
+        if (firstBracket === -1) continue;
+        const teacher = rest.substring(0, firstBracket).trim();
+
+        // 提取周次: [周次] 中的内容
+        const weekMatch = rest.match(/\[(\d+[-~,,\d]+)周\]/);
+        if (!weekMatch) {
+            console.log("HIT调试: 无周次, rest=" + rest.substring(0, 50));
+            continue;
+        }
+        const weeksStr = weekMatch[1];
+        const weeks = parseWeeks(weeksStr);
+        if (weeks.length === 0) continue;
+
+        // 提取节次: 最后一个[节次]节 中的内容
+        const sectionMatch = rest.match(/\[(.+)\]节/);
+        if (!sectionMatch) {
+            console.log("HIT调试: 无节次, rest=" + rest.substring(0, 50));
+            continue;
+        }
+        const sectionStr = sectionMatch[1].trim();
+
+        // 提取教室: 周次]和节次[之间的内容
+        const afterWeek = rest.substring(rest.indexOf(']周]') + 3);
+        const beforeSection = afterWeek.substring(0, afterWeek.lastIndexOf('['));
+        const room = beforeSection.trim();
+
+        const sectionParts = sectionStr.split(/[,,\s]+/);
+        const startSection = parseInt(sectionParts[0], 10);
+        const endSection = sectionParts.length > 1 ? parseInt(sectionParts[sectionParts.length - 1], 10) : startSection;
+        if (isNaN(startSection) || isNaN(endSection)) {
+            console.log("HIT调试: 节次解析失败, sectionStr=" + sectionStr);
+            continue;
+        }
+
+        console.log("HIT调试: 解析成功, " + courseName + "|" + teacher + "|" + room + "|d" + day + "|s" + startSection + "-" + endSection + "|w" + weeks.length);
+        courses.push({
+            name: courseName,
+            teacher: teacher,
+            position: room,
+            day: day,
+            startSection: startSection,
+            endSection: endSection,
+            weeks: weeks
+        });
+    }
+    return courses;
+}
+
+function mergeCourses(courses) {
+    if (courses.length <= 1) return courses;
+    courses.sort((a, b) => {
+        return a.name.localeCompare(b.name) || a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) || (a.day || 0) - (b.day || 0) ||
+               (a.startSection || 0) - (b.startSection || 0);
+    });
+    const merged = [];
+    let cur = courses[0];
+    for (let i = 1; i < courses.length; i++) {
+        const nxt = courses[i];
+        if (cur.name === nxt.name && cur.teacher === nxt.teacher && cur.position === nxt.position &&
+            cur.day === nxt.day && cur.startSection === nxt.startSection && cur.endSection === nxt.endSection) {
+            cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
+        } else {
+            merged.push(cur);
+            cur = nxt;
+        }
+    }
+    merged.push(cur);
+    return merged;
+}
+
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "研究生课表导入",
+        "导入前请确保您已在浏览器中成功登录研究生系统",
+        "好的,开始导入"
+    );
+}
+
+async function fetchCurrentSchedule() {
+    const url = "/xs/index/getDqxqkb?sf_request_type=ajax";
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: { "Content-Type": "application/x-www-form-urlencoded" },
+            credentials: "include"
+        });
+        if (!response.ok) return null;
+        const data = await response.json();
+        if (!data.isSuccess || !data.module) return null;
+        return data;
+    } catch (e) {
+        return null;
+    }
+}
+
+async function fetchSemesterInfo() {
+    const url = "/xs/index/getZcxx?sf_request_type=ajax";
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: { "Content-Type": "application/x-www-form-urlencoded" },
+            credentials: "include"
+        });
+        if (!response.ok) return null;
+        const data = await response.json();
+        if (!data.isSuccess || !data.module) return null;
+        return data.module;
+    } catch (e) {
+        return null;
+    }
+}
+
+const dayMap = { mon: 1, tues: 2, wed: 3, thur: 4, fri: 5, sat: 6, sun: 7 };
+
+function parseScheduleData(scheduleData) {
+    const allCourses = [];
+    const module = scheduleData.module;
+    console.log("HIT调试: module行数=" + module.length);
+    for (const row of module) {
+        const sections = row.jcmc;
+        for (const [dayKey, dayNum] of Object.entries(dayMap)) {
+            const cellStr = row[dayKey];
+            if (!cellStr || cellStr === 'null') continue;
+            console.log("HIT调试: " + dayKey + "=" + cellStr.substring(0, 50));
+            const courses = parseCourseCell(cellStr, dayNum, sections);
+            console.log("HIT调试: 解析出" + courses.length + "门课");
+            allCourses.push(...courses);
+        }
+    }
+    return mergeCourses(allCourses);
+}
+
+async function saveCourses(courses) {
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast("课程保存失败: " + error.message);
+        return false;
+    }
+}
+
+async function runImportFlow() {
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("用户取消了导入。");
+        return;
+    }
+
+    window.shiguangBridge.showToast("正在获取课表数据...");
+
+    const [scheduleData, semesterInfo] = await Promise.all([
+        fetchCurrentSchedule(),
+        fetchSemesterInfo()
+    ]);
+
+    if (!scheduleData) {
+        window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
+        return;
+    }
+
+    console.log("HIT调试: API返回成功, module长度=" + scheduleData.module.length);
+    console.log("HIT调试: 第一行数据=" + JSON.stringify(scheduleData.module[0]));
+
+    const courses = parseScheduleData(scheduleData);
+    console.log("HIT调试: 总共解析=" + courses.length + "门课");
+    if (courses.length > 0) {
+        console.log("HIT调试: 第一门=" + JSON.stringify(courses[0]));
+    }
+    if (courses.length === 0) {
+        window.shiguangBridge.showToast("未找到课程数据");
+        return;
+    }
+
+    const saveResult = await saveCourses(courses);
+    if (!saveResult) return;
+
+    if (semesterInfo && semesterInfo.ZCJSSJ) {
+        try {
+            await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
+                semesterStartDate: null,
+                semesterTotalWeeks: 20
+            }));
+        } catch (e) {}
+    }
+
+    const semesterName = semesterInfo ? semesterInfo.MC : "当前学期";
+    window.shiguangBridge.showToast("课程导入成功!" + semesterName + ",共 " + courses.length + " 门课程");
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();

+ 8 - 0
resources/HUEB/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "HUEB"
+    adapter_name: "河北经贸大学正方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "hueb.js"
+    import_url: "https://202-206-194-100.vpn.hueb.edu.cn:8118/jwglxt/xtgl/login_slogin.html"
+    maintainer: "Haooz"
+    description: "河北经贸大学正方教务V9,需通过VPN访问,进入教务系统后点击导入"

+ 457 - 0
resources/HUEB/hueb.js

@@ -0,0 +1,457 @@
+// 湖北工程学院(vpn.hueb.edu.cn) 拾光课程表适配脚本
+// 基于正方教务系统接口适配
+// 出现问题请提issues或者提交pr更改
+
+/**
+ * 节次与周次合并去重函数
+ * @param {Array<Object>} courses 原始解析课程数组
+ * @returns {Array<Object>} 合并去重后的课程数组
+ */
+function mergeAndDistinctCourses(courses) {
+    if (!Array.isArray(courses) || courses.length <= 1) return courses;
+
+    const list = courses.map(c => ({
+        ...c,
+        name: c.name || '',
+        teacher: c.teacher || '',
+        position: c.position || '',
+        weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
+    }));
+
+    list.sort((a, b) => {
+        return a.name.localeCompare(b.name) ||
+               a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) ||
+               (a.day || 0) - (b.day || 0) ||
+               a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
+               (a.startSection || 0) - (b.startSection || 0);
+    });
+
+    const step1Merged = [];
+    let current = list[0];
+    for (let i = 1; i < list.length; i++) {
+        const next = list[i];
+        const isSameCourseAndWeeks =
+            current.name === next.name &&
+            current.teacher === next.teacher &&
+            current.position === next.position &&
+            current.day === next.day &&
+            current.weeks.join(',') === next.weeks.join(',');
+
+        const isContinuous = current.endSection + 1 === next.startSection;
+        const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
+
+        if (isSameCourseAndWeeks && isContinuous) {
+            current.endSection = next.endSection;
+        } else if (isSameCourseAndWeeks && isDuplicate) {
+            continue;
+        } else {
+            step1Merged.push(current);
+            current = next;
+        }
+    }
+    step1Merged.push(current);
+
+    step1Merged.sort((a, b) => {
+        return a.name.localeCompare(b.name) ||
+               a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) ||
+               (a.day || 0) - (b.day || 0) ||
+               (a.startSection || 0) - (b.startSection || 0) ||
+               (a.endSection || 0) - (b.endSection || 0);
+    });
+
+    const step2Merged = [];
+    let cur = step1Merged[0];
+    for (let i = 1; i < step1Merged.length; i++) {
+        const nxt = step1Merged[i];
+        const isSameCourseAndSection =
+            cur.name === nxt.name &&
+            cur.teacher === nxt.teacher &&
+            cur.position === nxt.position &&
+            cur.day === nxt.day &&
+            cur.startSection === nxt.startSection &&
+            cur.endSection === nxt.endSection;
+
+        if (isSameCourseAndSection) {
+            cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
+        } else {
+            step2Merged.push(cur);
+            cur = nxt;
+        }
+    }
+    step2Merged.push(cur);
+
+    return step2Merged;
+}
+
+/**
+ * 解析周次字符串,处理单双周和周次范围
+ */
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+
+    const weekSets = weekStr.split(',');
+    let weeks = [];
+    for (const set of weekSets) {
+        const trimmedSet = set.trim();
+        const rangeMatch = trimmedSet.match(/(\d+)-(\d+)周/);
+        const singleMatch = trimmedSet.match(/^(\d+)周/);
+
+        let start = 0;
+        let end = 0;
+        let processed = false;
+
+        if (rangeMatch) {
+            start = Number(rangeMatch[1]);
+            end = Number(rangeMatch[2]);
+            processed = true;
+        } else if (singleMatch) {
+            start = end = Number(singleMatch[1]);
+            processed = true;
+        }
+
+        if (processed) {
+            const isSingle = trimmedSet.includes('(单)');
+            const isDouble = trimmedSet.includes('(双)');
+
+            for (let w = start; w <= end; w++) {
+                if (isSingle && w % 2 === 0) continue;
+                if (isDouble && w % 2 !== 0) continue;
+                weeks.push(w);
+            }
+        }
+    }
+
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+/**
+ * 解析 API 返回的 JSON 数据
+ */
+function parseJsonData(jsonData) {
+    if (!jsonData || !Array.isArray(jsonData.kbList)) {
+        return [];
+    }
+
+    const rawCourseList = jsonData.kbList;
+    const initialCourseList = [];
+
+    for (const rawCourse of rawCourseList) {
+        if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc ||
+            !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
+            continue;
+        }
+
+        const weeksArray = parseWeeks(rawCourse.zcd);
+        if (weeksArray.length === 0) {
+            continue;
+        }
+
+        const sectionParts = rawCourse.jcs.split('-');
+        const startSection = Number(sectionParts[0]);
+        const endSection = Number(sectionParts[sectionParts.length - 1]);
+        const day = Number(rawCourse.xqj);
+
+        if (isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
+            day < 1 || day > 7 || startSection > endSection) {
+            continue;
+        }
+
+        initialCourseList.push({
+            name: rawCourse.kcmc.trim(),
+            teacher: rawCourse.xm.trim(),
+            position: rawCourse.cdmc.trim(),
+            day: day,
+            startSection: startSection,
+            endSection: endSection,
+            weeks: weeksArray
+        });
+    }
+
+    return mergeAndDistinctCourses(initialCourseList);
+}
+
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "教务系统课表导入",
+        "导入前请确保您已在浏览器中成功登录教务系统",
+        "好的,开始导入"
+    );
+}
+
+/**
+ * 从教务系统获取学年学期选项
+ */
+async function fetchAcademicOptions() {
+    const url = "https://202-206-194-100.vpn.hueb.edu.cn:8118/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151";
+
+    try {
+        const response = await fetch(url, {
+            method: "GET",
+            credentials: "include"
+        });
+
+        if (!response.ok) return null;
+
+        const htmlText = await response.text();
+        const parser = new DOMParser();
+        const doc = parser.parseFromString(htmlText, "text/html");
+
+        const allYearOptions = Array.from(doc.querySelectorAll("#xnm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({
+                value: opt.value,
+                text: opt.textContent.trim(),
+                selected: opt.selected
+            }));
+
+        const semesterOptions = Array.from(doc.querySelectorAll("#xqm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({
+                value: opt.value,
+                text: opt.textContent.trim(),
+                selected: opt.selected
+            }));
+
+        if (allYearOptions.length === 0 || semesterOptions.length === 0) {
+            return null;
+        }
+
+        const selectedIndex = allYearOptions.findIndex(opt => opt.selected);
+
+        if (selectedIndex === -1) {
+            return {
+                yearOptions: allYearOptions.slice(0, 5),
+                semesterOptions,
+                defaultYearIndex: 0,
+                defaultSemesterIndex: semesterOptions.findIndex(opt => opt.selected) !== -1
+                    ? semesterOptions.findIndex(opt => opt.selected)
+                    : 0
+            };
+        }
+
+        const start = Math.max(0, selectedIndex - 2);
+        const end = Math.min(allYearOptions.length, selectedIndex + 3);
+        const yearOptions = allYearOptions.slice(start, end);
+        const newDefaultIndex = selectedIndex - start;
+        const defaultSemesterIndex = semesterOptions.findIndex(opt => opt.selected);
+
+        return {
+            yearOptions,
+            semesterOptions,
+            defaultYearIndex: newDefaultIndex,
+            defaultSemesterIndex: defaultSemesterIndex !== -1 ? defaultSemesterIndex : 0
+        };
+    } catch (e) {
+        return null;
+    }
+}
+
+/**
+ * 提示用户选择学年和学期
+ */
+async function selectAcademicYearAndSemester() {
+    const optionsData = await fetchAcademicOptions();
+
+    if (!optionsData) {
+        window.shiguangBridge.showToast("从教务系统读取学年学期失败,请确保登录状态。");
+        return null;
+    }
+
+    const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = optionsData;
+
+    const yearTexts = yearOptions.map(item => item.text);
+    const yearIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学年",
+        JSON.stringify(yearTexts),
+        defaultYearIndex
+    );
+
+    if (yearIndex === null || yearIndex === -1) return null;
+    const selectedYearCode = yearOptions[yearIndex].value;
+
+    const semesterTexts = semesterOptions.map(item => item.text);
+    const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesterTexts),
+        defaultSemesterIndex
+    );
+
+    if (semesterIndex === null || semesterIndex === -1) return null;
+    const selectedSemesterCode = semesterOptions[semesterIndex].value;
+
+    return {
+        academicYear: selectedYearCode,
+        semesterCode: selectedSemesterCode
+    };
+}
+
+/**
+ * 获取学期开学日期
+ */
+async function fetchSemesterStartDate(academicYear, semesterCode) {
+    const url = "https://202-206-194-100.vpn.hueb.edu.cn:8118/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154";
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}`;
+
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: {
+                "accept": "application/json, text/javascript, */*; q=0.01",
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
+                "x-requested-with": "XMLHttpRequest"
+            },
+            body: requestBody,
+            credentials: "include"
+        });
+
+        if (response.ok) {
+            const json = await response.json();
+            if (Array.isArray(json) && json.length > 0) {
+                const firstWeekObj = json.find(item => String(item.zs) === "1" || String(item.zsmc) === "1") || json[0];
+
+                if (firstWeekObj.rq) {
+                    const startDateStr = firstWeekObj.rq.split('/')[0];
+                    if (/^\d{4}-\d{2}-\d{2}$/.test(startDateStr)) {
+                        return startDateStr;
+                    }
+                }
+
+                if (firstWeekObj.zcrq) {
+                    const match = firstWeekObj.zcrq.match(/(\d{4}-\d{2}-\d{2})/);
+                    if (match) return match[1];
+                }
+
+                if (firstWeekObj.ksrq) {
+                    const match = firstWeekObj.ksrq.match(/(\d{4}-\d{2}-\d{2})/);
+                    if (match) return match[1];
+                }
+            }
+        }
+    } catch (e) {
+        // 获取失败不影响主流程
+    }
+    return null;
+}
+
+/**
+ * 请求和解析课程数据
+ */
+async function fetchAndParseCourses(academicYear, semesterCode) {
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=`;
+    const targetUrl = "https://202-206-194-100.vpn.hueb.edu.cn:8118/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151";
+
+    const [courseResponse, semesterStartDate] = await Promise.all([
+        fetch(targetUrl, {
+            method: "POST",
+            headers: {
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
+            },
+            body: requestBody,
+            credentials: "include"
+        }),
+        fetchSemesterStartDate(academicYear, semesterCode)
+    ]);
+
+    try {
+        if (courseResponse.ok) {
+            const jsonText = await courseResponse.text();
+            const jsonData = JSON.parse(jsonText);
+            if (jsonData && jsonData.kbList) {
+                const parsedCourses = parseJsonData(jsonData);
+                if (parsedCourses.length > 0) {
+                    return {
+                        courses: parsedCourses,
+                        config: {
+                            semesterStartDate: semesterStartDate,
+                            semesterTotalWeeks: 20
+                        }
+                    };
+                }
+            }
+        }
+    } catch (e) {
+        // 请求失败
+    }
+
+    window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
+    return null;
+}
+
+async function saveCourses(parsedCourses) {
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
+        return false;
+    }
+}
+
+const TimeSlots = [
+    { number: 1, startTime: "08:00", endTime: "08:45" },
+    { number: 2, startTime: "08:45", endTime: "09:30" },
+    { number: 3, startTime: "09:45", endTime: "10:30" },
+    { number: 4, startTime: "10:30", endTime: "11:15" },
+    { number: 5, startTime: "11:25", endTime: "12:10" },
+    { number: 6, startTime: "14:00", endTime: "14:45" },
+    { number: 7, startTime: "14:45", endTime: "15:30" },
+    { number: 8, startTime: "15:45", endTime: "16:30" },
+    { number: 9, startTime: "16:30", endTime: "17:15" },
+    { number: 10, startTime: "17:25", endTime: "18:10" },
+    { number: 11, startTime: "19:00", endTime: "19:45" },
+    { number: 12, startTime: "19:45", endTime: "20:30" },
+    { number: 13, startTime: "20:40", endTime: "21:25" }
+];
+
+async function importPresetTimeSlots(timeSlots) {
+    if (timeSlots.length === 0) return;
+    try { await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots)); } catch (e) {}
+}
+
+async function runImportFlow() {
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("用户取消了导入。");
+        return;
+    }
+
+    const selection = await selectAcademicYearAndSemester();
+    if (!selection) {
+        window.shiguangBridge.showToast("未选择学年学期,导入流程终止。");
+        return;
+    }
+
+    const { academicYear, semesterCode } = selection;
+
+    const result = await fetchAndParseCourses(academicYear, semesterCode);
+    if (result === null) {
+        return;
+    }
+
+    const { courses, config } = result;
+
+    const saveResult = await saveCourses(courses);
+    if (!saveResult) {
+        return;
+    }
+
+    try {
+        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+        let configMsg = "课表配置更新成功!";
+        if (config.semesterStartDate) {
+            configMsg += ` 开学日期:${config.semesterStartDate}`;
+        }
+        window.shiguangBridge.showToast(configMsg);
+    } catch (error) {
+        window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
+    }
+
+    await importPresetTimeSlots(TimeSlots);
+
+    window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();

+ 9 - 0
resources/JNMC/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/JNMC/adapters.yaml
+adapters:
+  - adapter_id: "JNMC_01"
+    adapter_name: "济宁医学院教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "jnmc.js"
+    import_url: "http://210.44.16.13/"
+    maintainer: "yukoimi"
+    description: "济宁医学院乘方教务适配,请先登录教务系统后再执行导入"

+ 179 - 0
resources/JNMC/jnmc.js

@@ -0,0 +1,179 @@
+// 济宁医学院教务(乘方教务 · 旧版 .action 接口)适配器
+
+const PRESET_TIME_SLOTS = [
+    { number: 1, startTime: "08:00", endTime: "08:40" },
+    { number: 2, startTime: "08:50", endTime: "09:30" },
+    { number: 3, startTime: "09:50", endTime: "10:30" },
+    { number: 4, startTime: "10:40", endTime: "11:20" },
+    { number: 5, startTime: "11:30", endTime: "12:10" },
+    { number: 6, startTime: "14:30", endTime: "15:10" },
+    { number: 7, startTime: "15:20", endTime: "16:00" },
+    { number: 8, startTime: "16:20", endTime: "17:00" },
+    { number: 9, startTime: "17:10", endTime: "17:50" },
+    { number: 10, startTime: "18:00", endTime: "18:40" },
+    { number: 11, startTime: "19:30", endTime: "20:10" },
+    { number: 12, startTime: "20:20", endTime: "21:00" }
+];
+
+// 周次字符串 "10,7,8,9" → 去重排序的周数组
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+    const weeks = weekStr.split(",").map(w => parseInt(w.trim(), 10)).filter(w => !isNaN(w) && w > 0);
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+// 教室可能为空、含 "\\" 分隔的多教室(如 "B205\206教室")或以 "," 分隔的多场地
+function resolvePosition(raw) {
+    const position = String(raw || "").replace(/\\/g, "/").trim();
+    return position || "待定";
+}
+
+// kbxx 课程 JSON → 拾光课程格式(合并同 key 课程的周次)
+function parseCourseList(kbxx) {
+    if (!Array.isArray(kbxx)) throw new Error("课表接口返回格式不正确");
+    const courseMap = new Map();
+    kbxx.forEach(item => {
+        const day = parseInt(item.xq, 10);
+        const sections = String(item.jcdm2 || "").split(",")
+            .map(s => parseInt(s.trim(), 10)).filter(s => !isNaN(s));
+        const allWeeks = parseWeeks(item.zcs);
+        if (!item.kcmc || sections.length === 0 || allWeeks.length === 0 || isNaN(day) || day < 1 || day > 7) return;
+
+        const course = {
+            name: item.kcmc.trim(),
+            teacher: String(item.teaxms || "").trim() || "未知",
+            position: resolvePosition(item.jxcdmcs),
+            day,
+            startSection: Math.min(...sections),
+            endSection: Math.max(...sections),
+            weeks: allWeeks
+        };
+
+        const key = [course.name, course.teacher, course.position, course.day,
+            course.startSection, course.endSection].join("__");
+        const existing = courseMap.get(key);
+        if (existing) existing.weeks = [...new Set([...existing.weeks, ...course.weeks])].sort((a, b) => a - b);
+        else courseMap.set(key, course);
+    });
+    return Array.from(courseMap.values()).sort((a, b) =>
+        a.day - b.day || a.startSection - b.startSection || a.endSection - b.endSection || a.name.localeCompare(b.name)
+    );
+}
+
+// 读取课表页中的学期下拉框
+function extractSemesterOptions(doc) {
+    const selectElem = doc.getElementById("xnxqdm");
+    if (!selectElem) return null;
+    const semesters = [];
+    const semesterValues = [];
+    let defaultIndex = 0;
+    Array.from(selectElem.querySelectorAll("option")).forEach(option => {
+        if (!option.value) return;
+        semesters.push(option.innerText.trim());
+        semesterValues.push(option.value);
+        if (option.selected || option.hasAttribute("selected")) defaultIndex = semesters.length - 1;
+    });
+    if (semesters.length === 0) return null;
+
+    const start = Math.max(0, defaultIndex - 1);
+    const end = Math.min(semesters.length, defaultIndex + 10);
+    return {
+        semesters: semesters.slice(start, end),
+        semesterValues: semesterValues.slice(start, end),
+        defaultIndex: defaultIndex - start
+    };
+}
+
+// 导入前提示先登录教务系统
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "济宁医学院教务导入",
+        "请先确保已登录教务系统,再继续导入。",
+        "我已登录"
+    );
+}
+
+// 选择学期
+async function selectSemester(semesterOptions) {
+    const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesterOptions.semesters),
+        semesterOptions.defaultIndex
+    );
+    if (selectedIndex === null || selectedIndex < 0) return null;
+    return {
+        label: semesterOptions.semesters[selectedIndex],
+        value: semesterOptions.semesterValues[selectedIndex]
+    };
+}
+
+// 拉取课表页 HTML(含学期列表)
+async function fetchSchedulePage() {
+    const response = await fetch("/xsgrkbcx!getXsgrbkList.action", { method: "GET", credentials: "include" });
+    if (!response.ok) throw new Error(`无法打开课表页面(HTTP ${response.status})`);
+    return response.text();
+}
+
+// 拉取指定学期课表(HTML 内嵌 var kbxx=[...])
+async function fetchCourseData(xnxqdm) {
+    const response = await fetch(
+        `/xsgrkbcx!xsAllKbList.action?xnxqdm=${encodeURIComponent(xnxqdm)}`,
+        { method: "GET", credentials: "include" }
+    );
+    if (!response.ok) throw new Error(`课表请求失败(HTTP ${response.status})`);
+    const htmlText = await response.text();
+    const match = htmlText.match(/var\s+kbxx\s*=\s*(\[[\s\S]*?\]);/);
+    if (!match) throw new Error("课表数据解析失败,请检查登录状态");
+    let kbxx;
+    try {
+        kbxx = JSON.parse(match[1]);
+    } catch (error) {
+        throw new Error(`课表数据解析失败:${error.message}`);
+    }
+    return kbxx;
+}
+
+// 主流程:提示 → 选学期 → 拉课表 → 保存课程与作息
+async function runImportFlow() {
+    try {
+        const confirmed = await promptUserToStart();
+        if (!confirmed) { window.shiguangBridge.showToast("导入已取消"); return; }
+
+        const pageHtml = await fetchSchedulePage();
+        const semesterOptions = extractSemesterOptions(new DOMParser().parseFromString(pageHtml, "text/html"));
+        if (!semesterOptions) throw new Error("未找到学期列表,请先登录教务系统");
+
+        const semester = await selectSemester(semesterOptions);
+        if (!semester) { window.shiguangBridge.showToast("导入已取消"); return; }
+
+        window.shiguangBridge.showToast(`正在获取 ${semester.label} 的课表...`);
+        const courses = parseCourseList(await fetchCourseData(semester.value));
+
+        if (courses.length === 0) {
+            await window.shiguangBridgePromise.showAlert(
+                "提示",
+                "该学期没有获取到课程数据,请检查登录状态和所选学期。",
+                "确定"
+            );
+            return;
+        }
+
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        try {
+            await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(PRESET_TIME_SLOTS));
+        } catch (error) {
+            window.shiguangBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
+        }
+
+        window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
+        window.shiguangBridge.notifyTaskCompletion();
+    } catch (error) {
+        await window.shiguangBridgePromise.showAlert(
+            "导入失败",
+            error.message || String(error),
+            "确定"
+        );
+    }
+}
+
+runImportFlow();

+ 8 - 0
resources/LCUDCC/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "LCUDCC"
+    adapter_name: "聊城大学东昌学院正方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "lcudcc.js"
+    import_url: "https://jw.lcudcc.edu.cn/jwglxt/xtgl/login_slogin.html"
+    maintainer: "Haooz"
+    description: "聊城大学东昌学院正方教务V9,进入教务系统后点击导入"

+ 438 - 0
resources/LCUDCC/lcudcc.js

@@ -0,0 +1,438 @@
+// 聊城大学东昌学院(lcudcc.edu.cn) 拾光课程表适配脚本
+// 基于正方教务系统接口适配
+// 出现问题请提issues或者提交pr更改
+
+/**
+ * 节次与周次合并去重函数
+ * @param {Array<Object>} courses 原始解析课程数组
+ * @returns {Array<Object>} 合并去重后的课程数组
+ */
+function mergeAndDistinctCourses(courses) {
+    if (!Array.isArray(courses) || courses.length <= 1) return courses;
+
+    // 1. 深拷贝并规范周次数据,过滤无效项
+    const list = courses.map(c => ({
+        ...c,
+        name: c.name || '',
+        teacher: c.teacher || '',
+        position: c.position || '',
+        weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
+    }));
+
+    // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
+    list.sort((a, b) => {
+        return a.name.localeCompare(b.name) ||
+               a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) ||
+               (a.day || 0) - (b.day || 0) ||
+               a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
+               (a.startSection || 0) - (b.startSection || 0);
+    });
+
+    const step1Merged = [];
+    let current = list[0];
+    for (let i = 1; i < list.length; i++) {
+        const next = list[i];
+        const isSameCourseAndWeeks =
+            current.name === next.name &&
+            current.teacher === next.teacher &&
+            current.position === next.position &&
+            current.day === next.day &&
+            current.weeks.join(',') === next.weeks.join(',');
+
+        const isContinuous = current.endSection + 1 === next.startSection;
+        const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
+
+        if (isSameCourseAndWeeks && isContinuous) {
+            current.endSection = next.endSection;
+        } else if (isSameCourseAndWeeks && isDuplicate) {
+            continue;
+        } else {
+            step1Merged.push(current);
+            current = next;
+        }
+    }
+    step1Merged.push(current);
+
+    // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
+    step1Merged.sort((a, b) => {
+        return a.name.localeCompare(b.name) ||
+               a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) ||
+               (a.day || 0) - (b.day || 0) ||
+               (a.startSection || 0) - (b.startSection || 0) ||
+               (a.endSection || 0) - (b.endSection || 0);
+    });
+
+    const step2Merged = [];
+    let cur = step1Merged[0];
+    for (let i = 1; i < step1Merged.length; i++) {
+        const nxt = step1Merged[i];
+        const isSameCourseAndSection =
+            cur.name === nxt.name &&
+            cur.teacher === nxt.teacher &&
+            cur.position === nxt.position &&
+            cur.day === nxt.day &&
+            cur.startSection === nxt.startSection &&
+            cur.endSection === nxt.endSection;
+
+        if (isSameCourseAndSection) {
+            cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
+        } else {
+            step2Merged.push(cur);
+            cur = nxt;
+        }
+    }
+    step2Merged.push(cur);
+
+    return step2Merged;
+}
+
+/**
+ * 解析周次字符串,处理单双周和周次范围
+ */
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+
+    const weekSets = weekStr.split(',');
+    let weeks = [];
+    for (const set of weekSets) {
+        const trimmedSet = set.trim();
+        const rangeMatch = trimmedSet.match(/(\d+)-(\d+)周/);
+        const singleMatch = trimmedSet.match(/^(\d+)周/);
+
+        let start = 0;
+        let end = 0;
+        let processed = false;
+
+        if (rangeMatch) {
+            start = Number(rangeMatch[1]);
+            end = Number(rangeMatch[2]);
+            processed = true;
+        } else if (singleMatch) {
+            start = end = Number(singleMatch[1]);
+            processed = true;
+        }
+
+        if (processed) {
+            const isSingle = trimmedSet.includes('(单)');
+            const isDouble = trimmedSet.includes('(双)');
+
+            for (let w = start; w <= end; w++) {
+                if (isSingle && w % 2 === 0) continue;
+                if (isDouble && w % 2 !== 0) continue;
+                weeks.push(w);
+            }
+        }
+    }
+
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+/**
+ * 解析 API 返回的 JSON 数据
+ */
+function parseJsonData(jsonData) {
+    if (!jsonData || !Array.isArray(jsonData.kbList)) {
+        return [];
+    }
+
+    const rawCourseList = jsonData.kbList;
+    const initialCourseList = [];
+
+    for (const rawCourse of rawCourseList) {
+        if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc ||
+            !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
+            continue;
+        }
+
+        const weeksArray = parseWeeks(rawCourse.zcd);
+        if (weeksArray.length === 0) {
+            continue;
+        }
+
+        const sectionParts = rawCourse.jcs.split('-');
+        const startSection = Number(sectionParts[0]);
+        const endSection = Number(sectionParts[sectionParts.length - 1]);
+        const day = Number(rawCourse.xqj);
+
+        if (isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
+            day < 1 || day > 7 || startSection > endSection) {
+            continue;
+        }
+
+        initialCourseList.push({
+            name: rawCourse.kcmc.trim(),
+            teacher: rawCourse.xm.trim(),
+            position: rawCourse.cdmc.trim(),
+            day: day,
+            startSection: startSection,
+            endSection: endSection,
+            weeks: weeksArray
+        });
+    }
+
+    return mergeAndDistinctCourses(initialCourseList);
+}
+
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "教务系统课表导入",
+        "导入前请确保您已在浏览器中成功登录教务系统",
+        "好的,开始导入"
+    );
+}
+
+/**
+ * 从教务系统获取学年学期选项
+ * 学年:以选中项为中心,取前2年+后2年,共5个选项
+ */
+async function fetchAcademicOptions() {
+    const url = "https://jw.lcudcc.edu.cn/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151";
+
+    try {
+        const response = await fetch(url, {
+            method: "GET",
+            credentials: "include"
+        });
+
+        if (!response.ok) return null;
+
+        const htmlText = await response.text();
+        const parser = new DOMParser();
+        const doc = parser.parseFromString(htmlText, "text/html");
+
+        const allYearOptions = Array.from(doc.querySelectorAll("#xnm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({
+                value: opt.value,
+                text: opt.textContent.trim(),
+                selected: opt.selected
+            }));
+
+        const semesterOptions = Array.from(doc.querySelectorAll("#xqm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({
+                value: opt.value,
+                text: opt.textContent.trim(),
+                selected: opt.selected
+            }));
+
+        if (allYearOptions.length === 0 || semesterOptions.length === 0) {
+            return null;
+        }
+
+        const selectedIndex = allYearOptions.findIndex(opt => opt.selected);
+
+        if (selectedIndex === -1) {
+            return {
+                yearOptions: allYearOptions.slice(0, 5),
+                semesterOptions,
+                defaultYearIndex: 0,
+                defaultSemesterIndex: semesterOptions.findIndex(opt => opt.selected) !== -1
+                    ? semesterOptions.findIndex(opt => opt.selected)
+                    : 0
+            };
+        }
+
+        const start = Math.max(0, selectedIndex - 2);
+        const end = Math.min(allYearOptions.length, selectedIndex + 3);
+        const yearOptions = allYearOptions.slice(start, end);
+        const newDefaultIndex = selectedIndex - start;
+        const defaultSemesterIndex = semesterOptions.findIndex(opt => opt.selected);
+
+        return {
+            yearOptions,
+            semesterOptions,
+            defaultYearIndex: newDefaultIndex,
+            defaultSemesterIndex: defaultSemesterIndex !== -1 ? defaultSemesterIndex : 0
+        };
+    } catch (e) {
+        return null;
+    }
+}
+
+/**
+ * 提示用户选择学年和学期
+ */
+async function selectAcademicYearAndSemester() {
+    const optionsData = await fetchAcademicOptions();
+
+    if (!optionsData) {
+        window.shiguangBridge.showToast("从教务系统读取学年学期失败,请确保登录状态。");
+        return null;
+    }
+
+    const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = optionsData;
+
+    const yearTexts = yearOptions.map(item => item.text);
+    const yearIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学年",
+        JSON.stringify(yearTexts),
+        defaultYearIndex
+    );
+
+    if (yearIndex === null || yearIndex === -1) return null;
+    const selectedYearCode = yearOptions[yearIndex].value;
+
+    const semesterTexts = semesterOptions.map(item => item.text);
+    const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesterTexts),
+        defaultSemesterIndex
+    );
+
+    if (semesterIndex === null || semesterIndex === -1) return null;
+    const selectedSemesterCode = semesterOptions[semesterIndex].value;
+
+    return {
+        academicYear: selectedYearCode,
+        semesterCode: selectedSemesterCode
+    };
+}
+
+/**
+ * 获取学期开学日期
+ */
+async function fetchSemesterStartDate(academicYear, semesterCode) {
+    const url = "https://jw.lcudcc.edu.cn/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154";
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}`;
+
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: {
+                "accept": "application/json, text/javascript, */*; q=0.01",
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
+                "x-requested-with": "XMLHttpRequest"
+            },
+            body: requestBody,
+            credentials: "include"
+        });
+
+        if (response.ok) {
+            const json = await response.json();
+            if (Array.isArray(json) && json.length > 0) {
+                const firstWeekObj = json.find(item => String(item.zs) === "1" || String(item.zsmc) === "1") || json[0];
+
+                if (firstWeekObj.rq) {
+                    const startDateStr = firstWeekObj.rq.split('/')[0];
+                    if (/^\d{4}-\d{2}-\d{2}$/.test(startDateStr)) {
+                        return startDateStr;
+                    }
+                }
+
+                if (firstWeekObj.zcrq) {
+                    const match = firstWeekObj.zcrq.match(/(\d{4}-\d{2}-\d{2})/);
+                    if (match) return match[1];
+                }
+
+                if (firstWeekObj.ksrq) {
+                    const match = firstWeekObj.ksrq.match(/(\d{4}-\d{2}-\d{2})/);
+                    if (match) return match[1];
+                }
+            }
+        }
+    } catch (e) {
+        // 获取失败不影响主流程
+    }
+    return null;
+}
+
+/**
+ * 请求和解析课程数据
+ */
+async function fetchAndParseCourses(academicYear, semesterCode) {
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=`;
+    const targetUrl = "https://jw.lcudcc.edu.cn/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151";
+
+    const [courseResponse, semesterStartDate] = await Promise.all([
+        fetch(targetUrl, {
+            method: "POST",
+            headers: {
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
+            },
+            body: requestBody,
+            credentials: "include"
+        }),
+        fetchSemesterStartDate(academicYear, semesterCode)
+    ]);
+
+    try {
+        if (courseResponse.ok) {
+            const jsonText = await courseResponse.text();
+            const jsonData = JSON.parse(jsonText);
+            if (jsonData && jsonData.kbList) {
+                const parsedCourses = parseJsonData(jsonData);
+                if (parsedCourses.length > 0) {
+                    return {
+                        courses: parsedCourses,
+                        config: {
+                            semesterStartDate: semesterStartDate,
+                            semesterTotalWeeks: 20
+                        }
+                    };
+                }
+            }
+        }
+    } catch (e) {
+        // 请求失败
+    }
+
+    window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
+    return null;
+}
+
+async function saveCourses(parsedCourses) {
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
+        return false;
+    }
+}
+
+async function runImportFlow() {
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("用户取消了导入。");
+        return;
+    }
+
+    const selection = await selectAcademicYearAndSemester();
+    if (!selection) {
+        window.shiguangBridge.showToast("未选择学年学期,导入流程终止。");
+        return;
+    }
+
+    const { academicYear, semesterCode } = selection;
+
+    const result = await fetchAndParseCourses(academicYear, semesterCode);
+    if (result === null) {
+        return;
+    }
+
+    const { courses, config } = result;
+
+    const saveResult = await saveCourses(courses);
+    if (!saveResult) {
+        return;
+    }
+
+    try {
+        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+        let configMsg = "课表配置更新成功!";
+        if (config.semesterStartDate) {
+            configMsg += ` 开学日期:${config.semesterStartDate}`;
+        }
+        window.shiguangBridge.showToast(configMsg);
+    } catch (error) {
+        window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
+    }
+
+    window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();

+ 8 - 0
resources/LYU/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "LYU"
+    adapter_name: "临沂大学正方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "lyu.js"
+    import_url: "https://sdp.lyu.edu.cn/https/webvpn3146b55e2aeee09087ebc4ebe6dbd770/jwglxt/xtgl/login_slogin.html"
+    maintainer: "Haooz"
+    description: "临沂大学正方教务V9,需通过webvpn访问,登录后进入课表页面点击导入"

+ 249 - 0
resources/LYU/lyu.js

@@ -0,0 +1,249 @@
+// 临沂大学(lyu.edu.cn) 拾光课程表适配脚本
+// 基于正方教务系统接口适配(webvpn版)
+
+function mergeAndDistinctCourses(courses) {
+    if (!Array.isArray(courses) || courses.length <= 1) return courses;
+    const list = courses.map(c => ({
+        ...c,
+        name: c.name || '',
+        teacher: c.teacher || '',
+        position: c.position || '',
+        weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
+    }));
+    list.sort((a, b) => {
+        return a.name.localeCompare(b.name) || a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) || (a.day || 0) - (b.day || 0) ||
+               a.weeks.join(',').localeCompare(b.weeks.join(',')) || (a.startSection || 0) - (b.startSection || 0);
+    });
+    const step1Merged = [];
+    let current = list[0];
+    for (let i = 1; i < list.length; i++) {
+        const next = list[i];
+        const isSame = current.name === next.name && current.teacher === next.teacher &&
+            current.position === next.position && current.day === next.day &&
+            current.weeks.join(',') === next.weeks.join(',');
+        if (isSame && current.endSection + 1 === next.startSection) {
+            current.endSection = next.endSection;
+        } else if (isSame && current.startSection === next.startSection && current.endSection === next.endSection) {
+            continue;
+        } else {
+            step1Merged.push(current);
+            current = next;
+        }
+    }
+    step1Merged.push(current);
+    step1Merged.sort((a, b) => {
+        return a.name.localeCompare(b.name) || a.teacher.localeCompare(b.teacher) ||
+               a.position.localeCompare(b.position) || (a.day || 0) - (b.day || 0) ||
+               (a.startSection || 0) - (b.startSection || 0) || (a.endSection || 0) - (b.endSection || 0);
+    });
+    const step2Merged = [];
+    let cur = step1Merged[0];
+    for (let i = 1; i < step1Merged.length; i++) {
+        const nxt = step1Merged[i];
+        if (cur.name === nxt.name && cur.teacher === nxt.teacher && cur.position === nxt.position &&
+            cur.day === nxt.day && cur.startSection === nxt.startSection && cur.endSection === nxt.endSection) {
+            cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
+        } else {
+            step2Merged.push(cur);
+            cur = nxt;
+        }
+    }
+    step2Merged.push(cur);
+    return step2Merged;
+}
+
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+    const weekSets = weekStr.split(',');
+    let weeks = [];
+    for (const set of weekSets) {
+        const trimmedSet = set.trim();
+        const rangeMatch = trimmedSet.match(/(\d+)-(\d+)周/);
+        const singleMatch = trimmedSet.match(/^(\d+)周/);
+        let start = 0, end = 0, processed = false;
+        if (rangeMatch) { start = Number(rangeMatch[1]); end = Number(rangeMatch[2]); processed = true; }
+        else if (singleMatch) { start = end = Number(singleMatch[1]); processed = true; }
+        if (processed) {
+            const isSingle = trimmedSet.includes('(单)');
+            const isDouble = trimmedSet.includes('(双)');
+            for (let w = start; w <= end; w++) {
+                if (isSingle && w % 2 === 0) continue;
+                if (isDouble && w % 2 !== 0) continue;
+                weeks.push(w);
+            }
+        }
+    }
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+function parseJsonData(jsonData) {
+    if (!jsonData || !Array.isArray(jsonData.kbList)) return [];
+    const initialCourseList = [];
+    for (const rawCourse of jsonData.kbList) {
+        if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc ||
+            !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) continue;
+        const weeksArray = parseWeeks(rawCourse.zcd);
+        if (weeksArray.length === 0) continue;
+        const sectionParts = rawCourse.jcs.split('-');
+        const startSection = Number(sectionParts[0]);
+        const endSection = Number(sectionParts[sectionParts.length - 1]);
+        const day = Number(rawCourse.xqj);
+        if (isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
+            day < 1 || day > 7 || startSection > endSection) continue;
+        initialCourseList.push({
+            name: rawCourse.kcmc.trim(),
+            teacher: rawCourse.xm.trim(),
+            position: rawCourse.cdmc.trim(),
+            day: day,
+            startSection: startSection,
+            endSection: endSection,
+            weeks: weeksArray
+        });
+    }
+    return mergeAndDistinctCourses(initialCourseList);
+}
+
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "教务系统课表导入",
+        "导入前请确保您已在浏览器中成功登录教务系统",
+        "好的,开始导入"
+    );
+}
+
+async function fetchAcademicOptions() {
+    const url = "/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&enlink-vpn";
+    try {
+        const response = await fetch(url, { method: "GET", credentials: "include" });
+        if (!response.ok) return null;
+        const htmlText = await response.text();
+        const parser = new DOMParser();
+        const doc = parser.parseFromString(htmlText, "text/html");
+        const allYearOptions = Array.from(doc.querySelectorAll("#xnm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({ value: opt.value, text: opt.textContent.trim(), selected: opt.hasAttribute("selected") }));
+        const semesterOptions = Array.from(doc.querySelectorAll("#xqm option"))
+            .filter(opt => opt.value !== "")
+            .map(opt => ({ value: opt.value, text: opt.textContent.trim(), selected: opt.hasAttribute("selected") }));
+        if (allYearOptions.length === 0 || semesterOptions.length === 0) return null;
+        const selectedIndex = allYearOptions.findIndex(opt => opt.selected);
+        if (selectedIndex === -1) {
+            return {
+                yearOptions: allYearOptions.slice(0, 5), semesterOptions,
+                defaultYearIndex: 0,
+                defaultSemesterIndex: semesterOptions.findIndex(opt => opt.selected) !== -1 ? semesterOptions.findIndex(opt => opt.selected) : 0
+            };
+        }
+        const start = Math.max(0, selectedIndex - 2);
+        const end = Math.min(allYearOptions.length, selectedIndex + 3);
+        return {
+            yearOptions: allYearOptions.slice(start, end), semesterOptions,
+            defaultYearIndex: selectedIndex - start,
+            defaultSemesterIndex: semesterOptions.findIndex(opt => opt.selected) !== -1 ? semesterOptions.findIndex(opt => opt.selected) : 0
+        };
+    } catch (e) { return null; }
+}
+
+async function selectAcademicYearAndSemester() {
+    const optionsData = await fetchAcademicOptions();
+    if (!optionsData) {
+        window.shiguangBridge.showToast("从教务系统读取学年学期失败,请确保登录状态。");
+        return null;
+    }
+    const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = optionsData;
+    const yearIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学年", JSON.stringify(yearOptions.map(item => item.text)), defaultYearIndex
+    );
+    if (yearIndex === null || yearIndex === -1) return null;
+    const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期", JSON.stringify(semesterOptions.map(item => item.text)), defaultSemesterIndex
+    );
+    if (semesterIndex === null || semesterIndex === -1) return null;
+    return { academicYear: yearOptions[yearIndex].value, semesterCode: semesterOptions[semesterIndex].value };
+}
+
+async function fetchSemesterStartDate(academicYear, semesterCode) {
+    const url = "/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154&enlink-vpn";
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: {
+                "accept": "application/json, text/javascript, */*; q=0.01",
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
+                "x-requested-with": "XMLHttpRequest"
+            },
+            body: `xnm=${academicYear}&xqm=${semesterCode}`,
+            credentials: "include"
+        });
+        if (response.ok) {
+            const json = await response.json();
+            if (Array.isArray(json) && json.length > 0) {
+                const firstWeekObj = json.find(item => String(item.zs) === "1" || String(item.zsmc) === "1") || json[0];
+                if (firstWeekObj.rq) {
+                    const startDateStr = firstWeekObj.rq.split('/')[0];
+                    if (/^\d{4}-\d{2}-\d{2}$/.test(startDateStr)) return startDateStr;
+                }
+                if (firstWeekObj.zcrq) {
+                    const match = firstWeekObj.zcrq.match(/(\d{4}-\d{2}-\d{2})/);
+                    if (match) return match[1];
+                }
+            }
+        }
+    } catch (e) {}
+    return null;
+}
+
+async function fetchAndParseCourses(academicYear, semesterCode) {
+    const targetUrl = "/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151&enlink-vpn";
+    const [courseResponse, semesterStartDate] = await Promise.all([
+        fetch(targetUrl, {
+            method: "POST",
+            headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" },
+            body: `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=`,
+            credentials: "include"
+        }),
+        fetchSemesterStartDate(academicYear, semesterCode)
+    ]);
+    try {
+        if (courseResponse.ok) {
+            const jsonData = await courseResponse.json();
+            if (jsonData && jsonData.kbList) {
+                const parsedCourses = parseJsonData(jsonData);
+                if (parsedCourses.length > 0) {
+                    return { courses: parsedCourses, config: { semesterStartDate: semesterStartDate, semesterTotalWeeks: 20 } };
+                }
+            }
+        }
+    } catch (e) {}
+    window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
+    return null;
+}
+
+async function saveCourses(parsedCourses) {
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast("课程保存失败: " + error.message);
+        return false;
+    }
+}
+
+async function runImportFlow() {
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) { window.shiguangBridge.showToast("用户取消了导入。"); return; }
+    const selection = await selectAcademicYearAndSemester();
+    if (!selection) { window.shiguangBridge.showToast("未选择学年学期,导入流程终止。"); return; }
+    const { academicYear, semesterCode } = selection;
+    const result = await fetchAndParseCourses(academicYear, semesterCode);
+    if (result === null) return;
+    const { courses, config } = result;
+    const saveResult = await saveCourses(courses);
+    if (!saveResult) return;
+    try { await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config)); } catch (e) {}
+    window.shiguangBridge.showToast("课程导入成功,共导入 " + courses.length + " 门课程!");
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();

+ 8 - 0
resources/SHXY/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "SHXY"
+    adapter_name: "绥化学院乘方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "shxy.js"
+    import_url: "http://jwgl.shxy.edu.cn/"
+    maintainer: "Haooz"
+    description: "绥化学院乘方教务系统,进入教务系统后点击导入"

+ 234 - 0
resources/SHXY/shxy.js

@@ -0,0 +1,234 @@
+// 绥化学院(shxy.edu.cn) 拾光课程表适配脚本
+// 基于乘方教务系统接口适配
+// 出现问题请提issues或者提交pr更改
+
+/**
+ * 解析周次字符串,例如 "1,2,3,4,5" -> [1, 2, 3, 4, 5]
+ */
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+    const weeks = weekStr.split(',')
+        .map(w => Number(w.trim()))
+        .filter(w => !isNaN(w) && w > 0);
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+/**
+ * 将教务系统返回的 JSON 转换为拾光标准的 CourseJsonModel 数组
+ */
+function parseJsonData(jsonData) {
+    console.log("JS: 开始解析课程 JSON...");
+    if (!jsonData || jsonData.code !== 0 || !Array.isArray(jsonData.data)) {
+        return [];
+    }
+
+    let allProcessedCourses = [];
+
+    jsonData.data.forEach(raw => {
+        // 基础字段校验
+        if (!(raw.kcmc && raw.xq && raw.ps && raw.pe)) return;
+
+        // 识别并解析 jxcdmc2
+        const locationMap = new Map();
+        if (raw.jxcdmc2) {
+            const parts = raw.jxcdmc2.split(',');
+            parts.forEach(part => {
+                const lastDashIndex = part.lastIndexOf('-');
+                if (lastDashIndex !== -1) {
+                    const room = part.substring(0, lastDashIndex).trim();
+                    const week = Number(part.substring(lastDashIndex + 1));
+                    if (!isNaN(week)) {
+                        if (!locationMap.has(room)) locationMap.set(room, []);
+                        locationMap.get(room).push(week);
+                    }
+                }
+            });
+        }
+
+        if (locationMap.size > 0) {
+            // 如果解析成功,则根据地点拆分为多个课程对象
+            locationMap.forEach((weeks, room) => {
+                allProcessedCourses.push({
+                    name: raw.kcmc.trim(),
+                    teacher: (raw.teaxms || "未知教师").trim(),
+                    position: room || "未知地点",
+                    day: Number(raw.xq),
+                    startSection: Number(raw.ps),
+                    endSection: Number(raw.pe),
+                    weeks: weeks.sort((a, b) => a - b)
+                });
+            });
+        } else if (raw.zc) {
+            // 兜底方案:如果 jxcdmc2 无效,使用原有的 jxcdmc 和 zc 逻辑
+            allProcessedCourses.push({
+                name: raw.kcmc.trim(),
+                teacher: (raw.teaxms || "未知教师").trim(),
+                position: (raw.jxcdmc || "未知地点").trim(),
+                day: Number(raw.xq),
+                startSection: Number(raw.ps),
+                endSection: Number(raw.pe),
+                weeks: parseWeeks(raw.zc)
+            });
+        }
+    });
+
+    return allProcessedCourses.filter(course => {
+        return course.weeks.length > 0 && course.startSection <= course.endSection;
+    });
+}
+
+/**
+ * 校验函数:验证用户输入的学年格式
+ */
+function validateYearInput(input) {
+    return /^[0-9]{4}$/.test(input) ? false : "请输入四位数字的起始学年(如 2025)";
+}
+
+/**
+ * 步骤 A: 显示引导公告
+ */
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "教务导入说明",
+        "1. 请确保已在浏览器中成功登录教务系统\n2. 导入过程中请勿关闭页面",
+        "好的,开始导入"
+    );
+}
+
+/**
+ * 步骤 B: 获取用户输入的学年
+ */
+async function getAcademicYear() {
+    const currentYear = new Date().getFullYear().toString();
+    return await window.shiguangBridgePromise.showPrompt(
+        "选择学年",
+        "请输入要导入的起始学年(例如 2025-2026 应输入2025):",
+        currentYear,
+        "validateYearInput"
+    );
+}
+
+/**
+ * 步骤 C: 选择学期
+ */
+async function selectSemester() {
+    const semesters = ["第一学期 (秋季)", "第二学期 (春季)"];
+    return await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesters),
+        0
+    );
+}
+
+/**
+ * 发起网络请求并获取数据
+ */
+async function fetchCourses(academicYear, semesterIndex) {
+    window.shiguangBridge.showToast("正在请求教务数据...");
+    
+    const semesterCode = semesterIndex === 0 ? "01" : "02"; 
+    const body = `xnxqdm=${academicYear}${semesterCode}`;
+    const url = "http://jwgl.shxy.edu.cn/new/student/xsgrkb/getCalendarWeekDatas";
+
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
+            body: body,
+            credentials: "include"
+        });
+
+        if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
+        
+        const jsonData = await response.json();
+        return parseJsonData(jsonData);
+    } catch (e) {
+        console.error("Fetch Error:", e);
+        window.shiguangBridge.showToast("网络请求失败,请检查登录状态");
+        return null;
+    }
+}
+
+const CampusTimeSlots = {
+    "老校区(厚德楼)": [
+        { number: 1, startTime: "08:10", endTime: "08:55" },
+        { number: 2, startTime: "09:00", endTime: "09:45" },
+        { number: 3, startTime: "10:15", endTime: "11:00" },
+        { number: 4, startTime: "11:05", endTime: "11:50" },
+        { number: 5, startTime: "13:45", endTime: "14:30" },
+        { number: 6, startTime: "14:35", endTime: "15:20" },
+        { number: 7, startTime: "15:40", endTime: "16:25" },
+        { number: 8, startTime: "16:30", endTime: "17:15" },
+        { number: 9, startTime: "18:30", endTime: "19:15" },
+        { number: 10, startTime: "19:20", endTime: "20:05" }
+    ],
+    "新校区(崇德楼)": [
+        { number: 1, startTime: "08:10", endTime: "08:55" },
+        { number: 2, startTime: "09:00", endTime: "09:45" },
+        { number: 3, startTime: "10:30", endTime: "11:15" },
+        { number: 4, startTime: "11:20", endTime: "12:05" },
+        { number: 5, startTime: "13:45", endTime: "14:30" },
+        { number: 6, startTime: "14:35", endTime: "15:20" },
+        { number: 7, startTime: "15:55", endTime: "16:40" },
+        { number: 8, startTime: "16:45", endTime: "17:30" },
+        { number: 9, startTime: "18:30", endTime: "19:15" },
+        { number: 10, startTime: "19:20", endTime: "20:05" }
+    ]
+};
+
+async function selectCampus() {
+    const campuses = Object.keys(CampusTimeSlots);
+    const idx = await window.shiguangBridgePromise.showSingleSelection(
+        "选择校区", JSON.stringify(campuses), 0
+    );
+    if (idx === null || idx === -1) return null;
+    return campuses[idx];
+}
+
+// 主流程编排
+async function runImportFlow() {
+    // 1. 前置检查
+    const isReady = await promptUserToStart();
+    if (!isReady) return;
+
+    // 2. 选择校区
+    const campus = await selectCampus();
+    if (!campus) {
+        window.shiguangBridge.showToast("未选择校区,导入终止。");
+        return;
+    }
+
+    // 3. 获取参数(学年、学期)
+    const year = await getAcademicYear();
+    if (!year) {
+        window.shiguangBridge.showToast("导入已取消");
+        return;
+    }
+
+    const semesterIdx = await selectSemester();
+    if (semesterIdx === null) {
+        window.shiguangBridge.showToast("导入已取消");
+        return;
+    }
+
+    // 4. 执行获取与解析
+    const courses = await fetchCourses(year, semesterIdx);
+    if (!courses || courses.length === 0) {
+        if (courses && courses.length === 0) window.shiguangBridge.showToast("该学期暂无课程数据");
+        return;
+    }
+
+    // 5. 数据保存
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(CampusTimeSlots[campus]));
+        window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!(${campus})`);
+        window.shiguangBridge.notifyTaskCompletion();
+        console.log("JS: 流程成功完成");
+    } catch (e) {
+        window.shiguangBridge.showToast("保存失败: " + e.message);
+    }
+}
+
+// 启动入口
+runImportFlow();

+ 8 - 0
resources/SMXY/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "SMXY"
+    adapter_name: "三明学院正方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "smxy.js"
+    import_url: "https://one.fjsmu.edu.cn/tp_up/view?m=up"
+    maintainer: "Haooz"
+    description: "三明学院正方教务V9,通过CAS统一身份认证登录后进入教务系统执行导入"

+ 182 - 0
resources/SMXY/smxy.js

@@ -0,0 +1,182 @@
+// 三明学院 (fjsmu.edu.cn) 拾光课程表适配脚本
+// 基于正方教务系统V9标准接口适配(CAS SSO版)
+
+function mergeAndDistinctCourses(courses) {
+    if (!Array.isArray(courses) || courses.length <= 1) return courses;
+    const list = courses.map(c => ({
+        ...c, name: c.name || '', teacher: c.teacher || '', position: c.position || '',
+        weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
+    }));
+    list.sort((a, b) => a.name.localeCompare(b.name) || a.teacher.localeCompare(b.teacher) ||
+        a.position.localeCompare(b.position) || (a.day || 0) - (b.day || 0) ||
+        a.weeks.join(',').localeCompare(b.weeks.join(',')) || (a.startSection || 0) - (b.startSection || 0));
+    const step1 = [];
+    let cur = list[0];
+    for (let i = 1; i < list.length; i++) {
+        const nxt = list[i];
+        const same = cur.name === nxt.name && cur.teacher === nxt.teacher && cur.position === nxt.position &&
+            cur.day === nxt.day && cur.weeks.join(',') === nxt.weeks.join(',');
+        if (same && cur.endSection + 1 === nxt.startSection) cur.endSection = nxt.endSection;
+        else if (same && cur.startSection === nxt.startSection && cur.endSection === nxt.endSection) continue;
+        else { step1.push(cur); cur = nxt; }
+    }
+    step1.push(cur);
+    step1.sort((a, b) => a.name.localeCompare(b.name) || a.teacher.localeCompare(b.teacher) ||
+        a.position.localeCompare(b.position) || (a.day || 0) - (b.day || 0) ||
+        (a.startSection || 0) - (b.startSection || 0) || (a.endSection || 0) - (b.endSection || 0));
+    const step2 = [];
+    cur = step1[0];
+    for (let i = 1; i < step1.length; i++) {
+        const nxt = step1[i];
+        if (cur.name === nxt.name && cur.teacher === nxt.teacher && cur.position === nxt.position &&
+            cur.day === nxt.day && cur.startSection === nxt.startSection && cur.endSection === nxt.endSection) {
+            cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
+        } else { step2.push(cur); cur = nxt; }
+    }
+    step2.push(cur);
+    return step2;
+}
+
+function parseWeeks(weekStr) {
+    if (!weekStr) return [];
+    const weeks = [];
+    for (const set of weekStr.split(',')) {
+        const s = set.trim();
+        const rangeM = s.match(/(\d+)-(\d+)周/);
+        const singleM = s.match(/^(\d+)周/);
+        let start = 0, end = 0, ok = false;
+        if (rangeM) { start = Number(rangeM[1]); end = Number(rangeM[2]); ok = true; }
+        else if (singleM) { start = end = Number(singleM[1]); ok = true; }
+        if (ok) {
+            const isSingle = s.includes('(单)'), isDouble = s.includes('(双)');
+            for (let w = start; w <= end; w++) {
+                if (isSingle && w % 2 === 0) continue;
+                if (isDouble && w % 2 !== 0) continue;
+                weeks.push(w);
+            }
+        }
+    }
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+function parseJsonData(jsonData) {
+    if (!jsonData || !Array.isArray(jsonData.kbList)) return [];
+    const list = [];
+    for (const c of jsonData.kbList) {
+        if (!c.kcmc || !c.xm || !c.cdmc || !c.xqj || !c.jcs || !c.zcd) continue;
+        const weeks = parseWeeks(c.zcd);
+        if (!weeks.length) continue;
+        const parts = c.jcs.split('-');
+        const start = Number(parts[0]), end = Number(parts[parts.length - 1]);
+        const day = Number(c.xqj);
+        if (isNaN(day) || isNaN(start) || isNaN(end) || day < 1 || day > 7 || start > end) continue;
+        list.push({ name: c.kcmc.trim(), teacher: c.xm.trim(), position: c.cdmc.trim(),
+            day, startSection: start, endSection: end, weeks });
+    }
+    return mergeAndDistinctCourses(list);
+}
+
+async function promptUserToStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "教务系统课表导入", "导入前请确保您已在浏览器中成功登录教务系统", "好的,开始导入");
+}
+
+async function fetchAcademicOptions() {
+    const url = "/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151";
+    try {
+        const resp = await fetch(url, { method: "GET", credentials: "include" });
+        if (!resp.ok) return null;
+        const html = await resp.text();
+        const doc = new DOMParser().parseFromString(html, "text/html");
+        const years = Array.from(doc.querySelectorAll("#xnm option")).filter(o => o.value !== "")
+            .map(o => ({ value: o.value, text: o.textContent.trim(), selected: o.hasAttribute("selected") }));
+        const semesters = Array.from(doc.querySelectorAll("#xqm option")).filter(o => o.value !== "")
+            .map(o => ({ value: o.value, text: o.textContent.trim(), selected: o.hasAttribute("selected") }));
+        if (!years.length || !semesters.length) return null;
+        const selIdx = years.findIndex(o => o.selected);
+        const start = Math.max(0, (selIdx === -1 ? 0 : selIdx) - 2);
+        const end = Math.min(years.length, (selIdx === -1 ? 0 : selIdx) + 3);
+        return { yearOptions: years.slice(start, end), semesterOptions: semesters,
+            defaultYearIndex: selIdx === -1 ? 0 : selIdx - start,
+            defaultSemesterIndex: semesters.findIndex(o => o.selected) !== -1 ? semesters.findIndex(o => o.selected) : 0 };
+    } catch (e) { return null; }
+}
+
+async function selectAcademicYearAndSemester() {
+    const data = await fetchAcademicOptions();
+    if (!data) { window.shiguangBridge.showToast("从教务系统读取学年学期失败,请确保已登录。"); return null; }
+    const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = data;
+    const yearIdx = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学年", JSON.stringify(yearOptions.map(o => o.text)), defaultYearIndex);
+    if (yearIdx === null || yearIdx === -1) return null;
+    const semIdx = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期", JSON.stringify(semesterOptions.map(o => o.text)), defaultSemesterIndex);
+    if (semIdx === null || semIdx === -1) return null;
+    return { academicYear: yearOptions[yearIdx].value, semesterCode: semesterOptions[semIdx].value };
+}
+
+async function fetchSemesterStartDate(academicYear, semesterCode) {
+    const url = "/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154";
+    try {
+        const resp = await fetch(url, {
+            method: "POST",
+            headers: { "accept": "application/json, text/javascript, */*; q=0.01",
+                "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
+                "x-requested-with": "XMLHttpRequest" },
+            body: `xnm=${academicYear}&xqm=${semesterCode}`,
+            credentials: "include"
+        });
+        if (resp.ok) {
+            const json = await resp.json();
+            if (Array.isArray(json) && json.length > 0) {
+                const first = json.find(i => String(i.zs) === "1" || String(i.zsmc) === "1") || json[0];
+                if (first.rq) { const d = first.rq.split('/')[0]; if (/^\d{4}-\d{2}-\d{2}$/.test(d)) return d; }
+                if (first.zcrq) { const m = first.zcrq.match(/(\d{4}-\d{2}-\d{2})/); if (m) return m[1]; }
+            }
+        }
+    } catch (e) {}
+    return null;
+}
+
+async function fetchAndParseCourses(academicYear, semesterCode) {
+    const url = "/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151";
+    const [resp, startDate] = await Promise.all([
+        fetch(url, { method: "POST",
+            headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" },
+            body: `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=`,
+            credentials: "include" }),
+        fetchSemesterStartDate(academicYear, semesterCode)
+    ]);
+    try {
+        if (resp.ok) {
+            const json = await resp.json();
+            if (json && json.kbList) {
+                const courses = parseJsonData(json);
+                if (courses.length > 0) return { courses, config: { semesterStartDate: startDate, semesterTotalWeeks: 20 } };
+            }
+        }
+    } catch (e) {}
+    window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
+    return null;
+}
+
+async function saveCourses(courses) {
+    try { await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses)); return true; }
+    catch (e) { window.shiguangBridge.showToast("课程保存失败: " + e.message); return false; }
+}
+
+async function runImportFlow() {
+    const ok = await promptUserToStart();
+    if (!ok) { window.shiguangBridge.showToast("用户取消了导入。"); return; }
+    const sel = await selectAcademicYearAndSemester();
+    if (!sel) { window.shiguangBridge.showToast("未选择学年学期,导入终止。"); return; }
+    const result = await fetchAndParseCourses(sel.academicYear, sel.semesterCode);
+    if (!result) return;
+    const saved = await saveCourses(result.courses);
+    if (!saved) return;
+    try { await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(result.config)); } catch (e) {}
+    window.shiguangBridge.showToast("课程导入成功,共导入 " + result.courses.length + " 门课程!");
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();