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

Merge pull request #608 from XingHeYuZhuan/pending

星河欲转 2 недель назад
Родитель
Сommit
392efada6d

+ 16 - 1
index/root_index.yaml

@@ -393,6 +393,11 @@ schools:
     initial: "G"
     resource_folder: "GLMU"
 
+  - id: "GXMU"
+    name: "广西医科大学"
+    initial: "G"
+    resource_folder: "GXMU"
+
   - id: "NJFU"
     name: "南京林业大学"
     initial: "N"
@@ -916,4 +921,14 @@ schools:
   - id: "WUST"
     name: "武汉科技大学"
     initial: "W"
-    resource_folder: "WUST"
+    resource_folder: "WUST"
+
+  - id: "NYNU"
+    name: "南阳师范学院"
+    initial: "N"
+    resource_folder: "NYNU"
+
+  - id: "NCHU"
+    name: "南昌航空大学"
+    initial: "N"
+    resource_folder: "NCHU"

+ 9 - 0
resources/GXMU/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/GXMU/adapters.yaml
+adapters:
+  - adapter_id: "GXMU_01"
+    adapter_name: "广西医科大学智慧教务系统"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "gxmu.js"
+    import_url: "https://cas.gxmu.edu.cn/lyuapServer/login"
+    maintainer: "Haooz"
+    description: "广西医科大学智慧教务系统课表导入,登录门户站点以后,进入教务系统,直接点击导入即可"

+ 240 - 0
resources/GXMU/gxmu.js

@@ -0,0 +1,240 @@
+/**
+ * 广西医科大学智慧教务系统课表导入适配脚本
+ * 通过 getCalendarWeekDatas 接口获取课表数据(POST,返回 JSON、全学期)
+ * 课程节次直接由接口字段 ps(起始节次) / pe(结束节次) 提供
+ */
+
+// 广西医科大学作息时间表(第1-12节)
+const GXMU_TIME_SLOTS = [
+    { number: 1,  startTime: "08:20", endTime: "09:00" },
+    { number: 2,  startTime: "09:05", endTime: "09:45" },
+    { number: 3,  startTime: "10:05", endTime: "10:45" },
+    { number: 4,  startTime: "10:50", endTime: "11:30" },
+    { number: 5,  startTime: "11:35", endTime: "12:15" },
+    { number: 6,  startTime: "14:30", endTime: "15:10" },
+    { number: 7,  startTime: "15:15", endTime: "15:55" },
+    { number: 8,  startTime: "16:15", endTime: "16:55" },
+    { number: 9,  startTime: "17:00", endTime: "17:40" },
+    { number: 10, startTime: "19:00", endTime: "19:40" },
+    { number: 11, startTime: "19:45", endTime: "20:25" },
+    { number: 12, startTime: "20:30", endTime: "21:10" }
+];
+
+function parseWeeks(weekStr) {
+    // 兼容逗号分段的周次;数字按数值排序去重
+    const weeks = [];
+    weekStr.split(',').forEach(part => {
+        part = part.trim();
+        const n = Number(part);
+        if (!isNaN(n) && n >= 1) weeks.push(n);
+    });
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+/**
+ * 解析接口返回的单条课程记录,映射为课表所需结构
+ */
+function mapCourseRecord(record) {
+    const weeks = parseWeeks(record.zc || "");
+    if (weeks.length === 0) return null;
+
+    const day = parseInt(record.qsxq, 10);
+    const startSection = parseInt(record.ps, 10);
+    const endSection = parseInt(record.pe, 10);
+    if (!day || day < 1 || day > 7) return null;
+    if (!startSection || isNaN(startSection)) return null;
+
+    return {
+        name: record.kcmc || "",
+        teacher: record.teaxms || "未知教师",
+        position: record.jxcdmc || "未知地点",
+        day: day,
+        startSection: startSection,
+        endSection: isNaN(endSection) ? startSection : endSection,
+        weeks: weeks
+    };
+}
+
+/**
+ * 从接口返回的 JSON 数据中提取并映射全部课程
+ */
+function transformSchedule(jsonData) {
+    console.log("JS: transformSchedule 正在解析课时数据...");
+
+    const data = jsonData && Array.isArray(jsonData.data) ? jsonData.data : [];
+    console.log(`JS: 接口返回 ${data.length} 条课程记录`);
+
+    const rawCourses = data
+        .map(mapCourseRecord)
+        .filter(Boolean);
+
+    console.log(`JS: 解析出 ${rawCourses.length} 条课程记录`);
+
+    // 同一门课会因周次分段生成多个课时记录,按组合键去重
+    const seen = new Set();
+    const courses = [];
+    for (const c of rawCourses) {
+        const key = `${c.name}|${c.day}|${c.startSection}|${c.endSection}|${c.weeks.join(',')}|${c.teacher}|${c.position}`;
+        if (seen.has(key)) continue;
+        seen.add(key);
+        courses.push(c);
+    }
+
+    console.log(`JS: 去重后剩 ${courses.length} 门课程`);
+    return courses;
+}
+
+function isLoginPage() {
+    const url = window.location.href;
+    return url.includes('login') || url.includes('lyuapServer');
+}
+
+function validateYearInput(input) {
+    if (/^[0-9]{4}$/.test(input)) return false;
+    return "请输入四位数字的学年!";
+}
+
+async function promptUserToStart() {
+    console.log("JS: 流程开始:显示公告。");
+    return await window.shiguangBridgePromise.showAlert(
+        "教务系统课表导入",
+        "导入前请确保您已在浏览器中成功登录教务系统",
+        "好的,开始导入"
+    );
+}
+
+async function getAcademicYear() {
+    const currentYear = new Date().getFullYear().toString();
+    return await window.shiguangBridgePromise.showPrompt(
+        "选择学年",
+        "请输入要导入课程的起始学年(例如 2025-2026 应输入2025,将匹配 202501 学期):",
+        currentYear,
+        "validateYearInput"
+    );
+}
+
+async function selectSemester() {
+    const semesters = ["第一学期 (0)", "第二学期 (1)"];
+    const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesters),
+        0
+    );
+    return semesterIndex;
+}
+
+async function fetchAndParseCourses(academicYear, semesterIndex) {
+    window.shiguangBridge.showToast("正在请求课表数据...");
+
+    const semesterCode = semesterIndex === 0 ? "01" : "02";
+    const xnxqdm = `${academicYear}${semesterCode}`;
+
+    // zc 传空表示全部周;d1/d2 为参考周起止日期,zc 为空时服务端返回全学期
+    const today = new Date();
+    const startOfWeek = new Date(today);
+    startOfWeek.setDate(today.getDate() - today.getDay() + 1);
+    const endOfWeek = new Date(startOfWeek);
+    endOfWeek.setDate(startOfWeek.getDate() + 6);
+
+    const pad = n => String(n).padStart(2, '0');
+    const fmtDate = d => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} 00:00:00`;
+    const body = `xnxqdm=${xnxqdm}&zc=&d1=${encodeURIComponent(fmtDate(startOfWeek))}&d2=${encodeURIComponent(fmtDate(endOfWeek))}`;
+
+    const url = "https://jwxt.gxmu.edu.cn/new/student/xsgrkb/getCalendarWeekDatas";
+    console.log(`JS: 请求课表接口: ${url}`);
+    console.log(`JS: 请求体: ${body}`);
+
+    try {
+        const response = await fetch(url, {
+            method: "POST",
+            headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
+            credentials: "include",
+            body: body
+        });
+
+        if (!response.ok) {
+            throw new Error(`网络请求失败。状态码: ${response.status}`);
+        }
+
+        const jsonData = await response.json();
+        const courses = transformSchedule(jsonData);
+
+        if (courses.length === 0) {
+            window.shiguangBridge.showToast("未找到任何课程数据,请检查所选学年学期是否正确。");
+            return null;
+        }
+
+        console.log(`JS: 课程数据解析成功,共找到 ${courses.length} 门课程。`);
+        return { courses };
+
+    } catch (error) {
+        window.shiguangBridge.showToast(`请求或解析失败: ${error.message}`);
+        console.error('JS: Fetch/Parse Error:', error);
+        return null;
+    }
+}
+
+async function saveCourses(parsedCourses) {
+    window.shiguangBridge.showToast(`正在保存 ${parsedCourses.length} 门课程...`);
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses, null, 2));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
+        console.error('JS: Save Courses Error:', error);
+        return false;
+    }
+}
+
+async function saveTimeSlots(timeSlots) {
+    if (!timeSlots || timeSlots.length === 0) return;
+    try {
+        await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+        console.log("JS: 作息时间保存成功");
+    } catch (error) {
+        console.error('JS: Save TimeSlots Error:', error);
+    }
+}
+
+async function runImportFlow() {
+    if (isLoginPage()) {
+        window.shiguangBridge.showToast("导入失败:请先登录教务系统!");
+        return;
+    }
+
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("用户取消了导入。");
+        return;
+    }
+
+    const academicYear = await getAcademicYear();
+    if (academicYear === null) {
+        window.shiguangBridge.showToast("导入已取消。");
+        return;
+    }
+
+    const semesterIndex = await selectSemester();
+    if (semesterIndex === null || semesterIndex === -1) {
+        window.shiguangBridge.showToast("导入已取消。");
+        return;
+    }
+
+    const result = await fetchAndParseCourses(academicYear, semesterIndex);
+    if (result === null) {
+        return;
+    }
+    const { courses } = result;
+
+    const saveResult = await saveCourses(courses);
+    if (!saveResult) {
+        return;
+    }
+
+    await saveTimeSlots(GXMU_TIME_SLOTS);
+
+    window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();

+ 9 - 0
resources/NCHU/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/NCHU/adapters.yaml
+adapters:
+  - adapter_id: "NCHU_01"
+    adapter_name: "南昌航空大学教务系统"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "nchu.js"
+    import_url: "http://jwc-publish2.jwc.nchu.edu.cn/"
+    maintainer: "Haooz"
+    description: "南昌航空大学强智教务系统课表导入(需要校园网访问),进入课表查询界面以后,先点击查询所有全部周,再执行导入"

+ 245 - 0
resources/NCHU/nchu.js

@@ -0,0 +1,245 @@
+// 文件: nchu.js
+// 南昌航空大学教务系统课程表导入脚本
+
+function isOnSchedulePage() {
+    const url = window.location.href;
+    return /jwc-publish2\.jwc\.nchu\.edu\.cn/i.test(url);
+}
+
+// 解析周数字符串,支持逗号/顿号分隔的多段与连续区间
+// 例:第4-6,8-19周 / 第5,7,9,11,13,15,17,19周 / 第9周 / 第13-16周
+function parseWeeks(timeStr) {
+    const weeks = new Set();
+    const segment = timeStr.match(/第\s*([\d、,\-\s]+)\s*周/);
+    if (!segment) return [];
+    segment[1].split(/[,、\s]+/).forEach(part => {
+        part = part.trim();
+        if (!part) return;
+        const range = part.match(/^(\d+)\s*-\s*(\d+)$/);
+        if (range) {
+            const start = parseInt(range[1], 10);
+            const end = parseInt(range[2], 10);
+            for (let i = start; i <= end; i++) weeks.add(i);
+        } else if (/^\d+$/.test(part)) {
+            weeks.add(parseInt(part, 10));
+        }
+    });
+    return Array.from(weeks).sort((a, b) => a - b);
+}
+
+// 解析节次字符串,兼容 "01~02小节" 与 "01~02节" 两种写法
+function parseSections(sectionStr) {
+    const match = sectionStr.match(/(\d{1,2})~(\d{1,2})(?:小)?节/);
+    if (match) {
+        return { start: parseInt(match[1], 10), end: parseInt(match[2], 10) };
+    }
+    return null;
+}
+
+// 从文档中提取课程数据
+function extractCoursesFromDoc(doc) {
+    const courses = [];
+    
+    const table = doc.querySelector('.time-table table');
+    if (!table) {
+        console.log('未找到课表表格');
+        return courses;
+    }
+    
+    const rows = table.querySelectorAll('tbody tr');
+    
+    rows.forEach(row => {
+        const cells = row.querySelectorAll('td');
+        if (cells.length < 2) return;
+        
+        // cells[0] 是节次列,cells[1..] 对应周一~周日
+        for (let i = 1; i < cells.length; i++) {
+            const day = i;
+            const cell = cells[i];
+            
+            // 一个格子里可能有多门课(多个 .item-box)
+            const itemBoxes = cell.querySelectorAll('.item-box');
+            itemBoxes.forEach(itemBox => {
+                parseItemBox(itemBox, day, courses);
+            });
+        }
+    });
+    
+    return courses;
+}
+
+// 解析单个 .item-box(一门课),其内部可能含多个时间段块
+function parseItemBox(itemBox, day, courses) {
+    // 课程全名:.item-box 内第一个 <p>
+    const courseP = itemBox.querySelector('p');
+    const name = courseP ? courseP.textContent.trim() : '';
+    if (!name) return;
+    
+    // 每个时间段块的构成:<p>课程名</p><div class="tch-name">…</div><div><span item1>教室</span><span item3>周次</span></div>
+    itemBox.querySelectorAll('.tch-name').forEach(tch => {
+        const segDiv = tch.nextElementSibling;
+        if (!segDiv) return;
+        
+        // 周次:该 div 中 item3.png 所在 span 的文本(如 "第13-16周 星期四")
+        const weekImg = segDiv.querySelector('img[src*="item3.png"]');
+        const timeInfo = weekImg && weekImg.parentElement ? weekImg.parentElement.textContent.trim() : '';
+        const weeks = parseWeeks(timeInfo);
+        if (weeks.length === 0) return;
+        
+        // 节次:从 tch-name 内 "01~02节" 的 span 获取
+        let sectionInfo = null;
+        tch.querySelectorAll('span').forEach(s => {
+            if (sectionInfo) return;
+            sectionInfo = parseSections(s.textContent);
+        });
+        if (!sectionInfo) return;
+        
+        // 教室:该 div 中 item1.png 所在 span 的文字(如 "博学楼F栋-F302")
+        let room = '';
+        const roomImg = segDiv.querySelector('img[src*="item1.png"]');
+        if (roomImg && roomImg.parentElement) {
+            room = roomImg.parentElement.textContent.trim();
+        }
+        
+        // 教师:在本块 tch-name 的 span 中找 "教师:xxx"(避免与学分/节次拼接)
+        let teacher = '';
+        tch.querySelectorAll('span').forEach(s => {
+            if (teacher) return;
+            const m = s.textContent.match(/教师[::]\s*(.+)/);
+            if (m) teacher = m[1].trim();
+        });
+        
+        courses.push({
+            name,
+            teacher,
+            position: room || '未指定',
+            day,
+            startSection: sectionInfo.start,
+            endSection: sectionInfo.end,
+            weeks
+        });
+    });
+}
+
+// 获取当前页面的课程(兼容课表页直接注入,或从首页 iframe 中取课表内容)
+function getCurrentWeekCourses() {
+    // 若当前文档自身就是课表页,直接解析
+    if (document.querySelector('.time-table table')) {
+        return extractCoursesFromDoc(document);
+    }
+    
+    // 否则遍历 iframe 查找课表页
+    const iframes = Array.from(document.querySelectorAll('iframe'));
+    for (const iframe of iframes) {
+        try {
+            if (iframe.contentDocument && iframe.contentDocument.querySelector('.time-table table')) {
+                return extractCoursesFromDoc(iframe.contentDocument);
+            }
+        } catch (e) {
+            // 跨域 iframe 跳过
+        }
+    }
+    
+    return [];
+}
+
+// 去重合并课程
+function mergeAndDeduplicateCourses(allCourses) {
+    const courseMap = new Map();
+    
+    allCourses.forEach(course => {
+        const key = `${course.day}-${course.startSection}-${course.endSection}-${course.name}-${course.teacher}-${course.position}`;
+        
+        if (!courseMap.has(key)) {
+            courseMap.set(key, {
+                ...course,
+                weeks: [...course.weeks]
+            });
+        } else {
+            const existing = courseMap.get(key);
+            const weekSet = new Set([...existing.weeks, ...course.weeks]);
+            existing.weeks = Array.from(weekSet).sort((a, b) => a - b);
+        }
+    });
+    
+    return Array.from(courseMap.values());
+}
+
+// 生成时间段配置(该校实际为 11 节课)
+function generateTimeSlots() {
+    return [
+        { "number": 1, "startTime": "08:00", "endTime": "08:45" },
+        { "number": 2, "startTime": "08:55", "endTime": "09:40" },
+        { "number": 3, "startTime": "10:00", "endTime": "10:45" },
+        { "number": 4, "startTime": "10:55", "endTime": "11:40" },
+        { "number": 5, "startTime": "14:00", "endTime": "14:45" },
+        { "number": 6, "startTime": "14:55", "endTime": "15:40" },
+        { "number": 7, "startTime": "16:00", "endTime": "16:45" },
+        { "number": 8, "startTime": "16:55", "endTime": "17:40" },
+        { "number": 9, "startTime": "19:00", "endTime": "19:45" },
+        { "number": 10, "startTime": "19:55", "endTime": "20:40" },
+        { "number": 11, "startTime": "20:50", "endTime": "21:35" }
+    ];
+}
+
+// 主函数:导入课程
+async function importCourseSchedule() {
+    try {
+        console.log('开始导入课程表...');
+        window.shiguangBridge.showToast('正在获取课表数据...');
+        
+        // 获取当前周课程
+        const courses = getCurrentWeekCourses();
+        console.log(`找到 ${courses.length} 门课程`);
+        
+        if (courses.length === 0) {
+            window.shiguangBridge.showToast('未找到课程数据');
+            return false;
+        }
+        
+        console.log('课程数据:', courses);
+        
+        // 导入课程
+        const coursesResult = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        if (coursesResult === true) {
+            console.log('课程导入成功!');
+            window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
+        } else {
+            console.log('课程导入失败');
+            window.shiguangBridge.showToast('课程导入失败');
+            return false;
+        }
+        
+        // 生成并导入时间段
+        const finalTimeSlots = generateTimeSlots();
+        const timeSlotsResult = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(finalTimeSlots));
+        if (timeSlotsResult === true) {
+            console.log('时间段导入成功!');
+        }
+        
+        return true;
+        
+    } catch (error) {
+        console.error('导入过程出错:', error);
+        window.shiguangBridge.showToast('导入失败: ' + error.message);
+        return false;
+    }
+}
+
+// ========== 主执行逻辑 ==========
+
+if (isOnSchedulePage()) {
+    console.log('检测到南昌航空大学教务系统');
+    window.shiguangBridge.showToast('正在准备导入课程表...');
+    
+    setTimeout(async () => {
+        const success = await importCourseSchedule();
+        if (success) {
+            window.shiguangBridge.notifyTaskCompletion();
+        }
+    }, 2000);
+    
+} else {
+    console.log('当前不在教务系统页面');
+    window.shiguangBridge.showToast('请先登录教务系统!');
+}

+ 9 - 0
resources/NYNU/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/NYNU/adapters.yaml
+adapters:
+  - adapter_id: "NYNU_01"
+    adapter_name: "南阳师范学院教务系统"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "nynu.js"
+    import_url: "https://vpn.nynu.edu.cn/"
+    maintainer: "Haooz"
+    description: "南阳师范学院青果教务系统课表导入,需先登录学校VPN并通过统一身份认证进入教务系统后执行导入"

+ 238 - 0
resources/NYNU/nynu.js

@@ -0,0 +1,238 @@
+/**
+ * 南阳师范学院教务系统课表导入适配脚本
+ * 青果软件系统,通过 showLessonScheduleInfosV14.action 接口获取课表
+ * 课程信息分布在 id 形如 weekly0X_Y 的 div 中(X 为星期,Y 为节次单元)
+ */
+
+function parseWeeks(weekStr) {
+    // 兼容 "1-2,5-18" 逗号分段 + 区间
+    const weeks = [];
+    weekStr.split(',').forEach(part => {
+        part = part.trim();
+        if (part.includes('-')) {
+            const dashSegs = part.split('-').map(Number);
+            if (dashSegs.length === 2 && !isNaN(dashSegs[0]) && !isNaN(dashSegs[1])) {
+                for (let i = dashSegs[0]; i <= dashSegs[1]; i++) weeks.push(i);
+            }
+        } else if (!isNaN(Number(part))) {
+            weeks.push(Number(part));
+        }
+    });
+    return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+/**
+ * 解析单个 weekly 块里的 li 文本,返回课程信息
+ */
+function parseWeeklyDiv(divId, liTexts) {
+    // id 形如 weekly03_1 -> 星期=3,节次单元=1
+    const m = divId.match(/^weekly0?(\d+)_(\d+)$/);
+    if (!m) return null;
+    const day = parseInt(m[1], 10);
+    if (day < 1 || day > 7) return null;
+
+    let name = "", teacher = "", timeText = "", position = "";
+    for (const t of liTexts) {
+        const str = t.trim();
+        if (str.startsWith("课程名称")) name = str.replace(/^课程名称:/, "").replace(/^<b>/, "").replace(/<\/b>$/, "");
+        else if (str.startsWith("任课教师")) teacher = str.replace(/^任课教师:/, "").replace(/^<b>/, "").replace(/<\/b>$/, "");
+        else if (str.startsWith("上课时间")) timeText = str.replace(/^上课时间:/, "").replace(/^<b>/, "").replace(/<\/b>$/, "");
+        else if (str.startsWith("上课地点")) position = str.replace(/^上课地点:/, "").replace(/^<b>/, "").replace(/<\/b>$/, "");
+    }
+
+    if (!name || !timeText) return null;
+
+    // 上课时间形如:[1-2,5-18周] 三[1-2节]
+    const weekMatch = timeText.match(/\[([\d,\-]+)周\]/);
+    const sectionMatch = timeText.match(/\[(\d+)(?:-(\d+))?节\]/);
+    if (!weekMatch || !sectionMatch) return null;
+
+    const weeks = parseWeeks(weekMatch[1]);
+    const startSection = parseInt(sectionMatch[1], 10);
+    const endSection = sectionMatch[2] ? parseInt(sectionMatch[2], 10) : startSection;
+
+    if (weeks.length === 0) return null;
+
+    return {
+        name: name,
+        teacher: teacher || "未知教师",
+        position: position || "未知地点",
+        day: day,
+        startSection: startSection,
+        endSection: endSection,
+        weeks: weeks
+    };
+}
+
+/**
+ * 从接口返回的 HTML 中提取所有 weekly div 并解析课程
+ */
+function transformSchedule(htmlString) {
+    console.log("JS: transformSchedule 正在解析 HTML...");
+
+    const tempDoc = new DOMParser().parseFromString(htmlString, "text/html");
+
+    // 只取包裹课程详情的 weekly div(跳过表格等内容)
+    const weekDivs = Array.from(tempDoc.querySelectorAll('[id^="weekly"]'))
+        .filter(d => d.classList.contains("weeklesson"));
+
+    console.log(`JS: 找到 ${weekDivs.length} 个 weekly 课程块`);
+
+    const rawCourses = [];
+
+    weekDivs.forEach(div => {
+        const divId = div.id;
+        const lis = Array.from(div.querySelectorAll("ul li"));
+        const liTexts = lis.map(li => li.innerHTML);
+        const course = parseWeeklyDiv(divId, liTexts);
+        if (course) rawCourses.push(course);
+    });
+
+    console.log(`JS: 解析出 ${rawCourses.length} 条课程记录`);
+
+    // 青果系统同一门课会因节次格/周次分段生成重复的 weekly 块,
+    // 按组合键去重(同一课程、同时段、同周次只保留一条)。
+    const seen = new Set();
+    const courses = [];
+    for (const c of rawCourses) {
+        const key = `${c.name}|${c.day}|${c.startSection}|${c.endSection}|${c.weeks.join(',')}|${c.teacher}|${c.position}`;
+        if (seen.has(key)) continue;
+        seen.add(key);
+        courses.push(c);
+    }
+
+    console.log(`JS: 去重后剩 ${courses.length} 门课程`);
+    return courses;
+}
+
+function isLoginPage() {
+    const url = window.location.href;
+    return url.includes('login') || url.includes('cas');
+}
+
+function validateYearInput(input) {
+    if (/^[0-9]{4}$/.test(input)) return false;
+    return "请输入四位数字的学年!";
+}
+
+async function promptUserToStart() {
+    console.log("JS: 流程开始:显示公告。");
+    return await window.shiguangBridgePromise.showAlert(
+        "教务系统课表导入",
+        "导入前请确保您已在浏览器中成功登录教务系统",
+        "好的,开始导入"
+    );
+}
+
+async function getAcademicYear() {
+    const currentYear = new Date().getFullYear().toString();
+    return await window.shiguangBridgePromise.showPrompt(
+        "选择学年",
+        "请输入要导入课程的起始学年(例如 2025-2026 应输入2025):",
+        currentYear,
+        "validateYearInput"
+    );
+}
+
+async function selectSemester() {
+    const semesters = ["第一学期 (0)", "第二学期 (1)"];
+    const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesters),
+        0
+    );
+    return semesterIndex;
+}
+
+async function fetchAndParseCourses(academicYear, semesterIndex) {
+    window.shiguangBridge.showToast("正在请求课表数据...");
+
+    const semesterCode = semesterIndex === 0 ? "0" : "1";
+    // 从当前页面 URL 提取 vpn-12-o1-{源站} 形式的后缀(如 nysyjw.nynu.edu.cn),
+    // 不同登录会话源站域名可能不同;没匹配到时回退到教务源站固定值。
+    const originMatch = window.location.href.match(/vpn-12-o1-([a-zA-Z0-9.\-]+(?:\.edu\.cn)?)/);
+    const originHost = originMatch ? originMatch[1] : "nysyjw.nynu.edu.cn";
+    const baseUrl = "https://vpn.nynu.edu.cn/http/77726476706e69737468656265737421feee52852d27265e67069ce29d51367b58d2/nysfjw/frame/desk/showLessonScheduleInfosV14.action";
+    const url = `${baseUrl}?vpn-12-o1-${originHost}&xn=${academicYear}&xq=${semesterCode}`;
+    console.log(`JS: 请求课表接口: ${url}`);
+
+    try {
+        const response = await fetch(url, {
+            method: "GET",
+            credentials: "include"
+        });
+
+        if (!response.ok) {
+            throw new Error(`网络请求失败。状态码: ${response.status}`);
+        }
+
+        const htmlText = await response.text();
+        const courses = transformSchedule(htmlText);
+
+        if (courses.length === 0) {
+            window.shiguangBridge.showToast("未找到任何课程数据,请检查所选学年学期是否正确。");
+            return null;
+        }
+
+        console.log(`JS: 课程数据解析成功,共找到 ${courses.length} 门课程。`);
+        return { courses };
+
+    } catch (error) {
+        window.shiguangBridge.showToast(`请求或解析失败: ${error.message}`);
+        console.error('JS: Fetch/Parse Error:', error);
+        return null;
+    }
+}
+
+async function saveCourses(parsedCourses) {
+    window.shiguangBridge.showToast(`正在保存 ${parsedCourses.length} 门课程...`);
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses, null, 2));
+        return true;
+    } catch (error) {
+        window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
+        console.error('JS: Save Courses Error:', error);
+        return false;
+    }
+}
+
+async function runImportFlow() {
+    if (isLoginPage()) {
+        window.shiguangBridge.showToast("导入失败:请先登录教务系统!");
+        return;
+    }
+
+    const alertConfirmed = await promptUserToStart();
+    if (!alertConfirmed) {
+        window.shiguangBridge.showToast("用户取消了导入。");
+        return;
+    }
+
+    const academicYear = await getAcademicYear();
+    if (academicYear === null) {
+        window.shiguangBridge.showToast("导入已取消。");
+        return;
+    }
+
+    const semesterIndex = await selectSemester();
+    if (semesterIndex === null || semesterIndex === -1) {
+        window.shiguangBridge.showToast("导入已取消。");
+        return;
+    }
+
+    const result = await fetchAndParseCourses(academicYear, semesterIndex);
+    if (result === null) {
+        return;
+    }
+    const { courses } = result;
+
+    const saveResult = await saveCourses(courses);
+    if (!saveResult) {
+        return;
+    }
+
+    window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();