Przeglądaj źródła

Merge pull request #508 from XingHeYuZhuan/pending

星河欲转 3 tygodni temu
rodzic
commit
b5b606e0b9

+ 6 - 1
index/root_index.yaml

@@ -756,4 +756,9 @@ schools:
   - id: "SYSU"
     name: "中山大学"
     initial: "Z"
-    resource_folder: "SYSU"
+    resource_folder: "SYSU"
+
+  - id: "SXDZKJ"
+    name: "山西电子科技学院"
+    initial: "S"
+    resource_folder: "SXDZKJ"

+ 9 - 0
resources/SXDZKJ/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/SXDZKJ/adapters.yaml
+adapters:
+  - adapter_id: "SXDZKJ"
+    adapter_name: "山西电子科技学院青果教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "sxdzkj.js"
+    import_url: "https://sso.sxdzkj.edu.cn/sso-auth/#/login?service=https://jwxt.sxdzkj.edu.cn/caslogin"
+    maintainer: "REnss3536"
+    description: "山西电子科技学院青果教务系统适配,登录教务系统后进入课表查询页面进行导入"

+ 276 - 0
resources/SXDZKJ/sxdzkj.js

@@ -0,0 +1,276 @@
+// 山西电子科技学院(sxdzkj.edu.cn) 拾光课程表适配脚本
+// 基于青果教务系统适配
+// 适配器ID: SXDZKJ
+
+async function checkLoginEnvironment() {
+    const currentUrl = window.location.href;
+    if (currentUrl.includes("sso.sxdzkj.edu.cn/sso-auth") || currentUrl.includes("cas/login")) {
+        window.shiguangBridge.showToast("请先登录教务系统再进行导入");
+        return false;
+    }
+    return true;
+}
+
+function parseWeeks(weekStr) {
+    const weeks = [];
+    const groups = weekStr.split(',');
+    groups.forEach(group => {
+        const isSingle = group.includes('单');
+        const isDouble = group.includes('双');
+        const rangeMatch = group.match(/(\d+)-(\d+)/);
+        
+        if (rangeMatch) {
+            const start = parseInt(rangeMatch[1]);
+            const end = parseInt(rangeMatch[2]);
+            for (let i = start; i <= end; i++) {
+                if (isSingle && i % 2 === 0) continue;
+                if (isDouble && i % 2 !== 0) continue;
+                weeks.push(i);
+            }
+        } else {
+            const num = parseInt(group.replace(/[^\d]/g, ''));
+            if (!isNaN(num)) weeks.push(num);
+        }
+    });
+    return Array.from(new Set(weeks)).sort((a, b) => a - b);
+}
+
+function encodeParams(params) {
+    return btoa(params);
+}
+
+const dayMap = {
+    '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '日': 7, '天': 7
+};
+
+function parseScheduleString(scheduleStr, teacher, courseName) {
+    const results = [];
+    if (!scheduleStr || scheduleStr.trim() === '') return results;
+
+    const sessions = scheduleStr.split(';').map(s => s.trim()).filter(s => s);
+
+    sessions.forEach(session => {
+        // 有地点: "1-16周 三[7-8] 南-317(115)" / "1-16周(单) 一[5-6] 南-311(115)"
+        // 无地点: "1-16周 三[5-6]"
+        const match = session.match(/([\d\-]+周(?:\([单双]\))?)\s+([一二三四五六日天])\[(\d+\-\d+)\](?:\s*(.*))?/);
+        if (match) {
+            const weekStr = match[1];
+            const dayChar = match[2];
+            const sectionStr = match[3];
+            const position = (match[4] || "").trim();
+
+            const weeks = parseWeeks(weekStr);
+            const day = dayMap[dayChar];
+            const sections = sectionStr.split('-').map(Number);
+
+            if (weeks.length > 0 && day && sections.length === 2) {
+                results.push({
+                    name: courseName,
+                    teacher: teacher,
+                    position: position,
+                    day: day,
+                    startSection: sections[0],
+                    endSection: sections[1],
+                    weeks: weeks
+                });
+            }
+        }
+    });
+
+    return results;
+}
+
+function parseCourseData(htmlText) {
+    const parser = new DOMParser();
+    const doc = parser.parseFromString(htmlText, 'text/html');
+    const allCourses = [];
+
+    const allTables = doc.querySelectorAll('table');
+    if (allTables.length === 0) {
+        console.warn("未找到表格");
+        return [];
+    }
+
+    // 课程列表(含上课时间地点列)
+    allTables.forEach(table => {
+        if (!/上课时间地点/.test(table.textContent)) return;
+        allCourses.push(...parseCourseList(table));
+    });
+
+    console.log("解析完成,共课程条目:", allCourses.length);
+    return allCourses;
+}
+
+function parseCourseList(table) {
+    const courses = [];
+    const rows = table.querySelectorAll('tr');
+    rows.forEach(row => {
+        const cells = row.querySelectorAll('td');
+        // 课程列表:第3列课程、第7列教师、第11列时间地点
+        if (cells.length < 11) return;
+        const courseCell = cells[2];
+        const teacherCell = cells[6];
+        const scheduleCell = cells[10];
+        const courseName = courseCell.textContent.trim().replace(/\[.*?\]/g, '');
+        const teacher = teacherCell.textContent.trim().replace(/\[.*?\]/g, '');
+        const scheduleStr = scheduleCell.textContent.trim();
+        if (!scheduleStr) return;
+        const parsed = parseScheduleString(scheduleStr, teacher, courseName);
+        courses.push(...parsed);
+    });
+    return courses;
+}
+
+async function getYearAndSemester() {
+    const currentYear = new Date().getFullYear();
+    const yearStr = await window.shiguangBridgePromise.showPrompt(
+        "输入学年",
+        "请输入学年(如 2025-2026 输入2025):",
+        currentYear.toString(),
+        "validateYearInput"
+    );
+    
+    if (yearStr === null) return null;
+    
+    const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(["第一学期", "第二学期"]),
+        0
+    );
+    
+    if (semesterIndex === null) return null;
+    
+    return {
+        xn: yearStr,
+        xq: semesterIndex === 0 ? "0" : "1"
+    };
+}
+
+function validateYearInput(input) {
+    if (/^[0-9]{4}$/.test(input)) {
+        return false;
+    } else {
+        return "请输入四位数字的学年!";
+    }
+}
+
+async function fetchCourses(xn, xq) {
+    try {
+        window.shiguangBridge.showToast("正在获取课表数据...");
+        
+        let xh = "";
+        try {
+            const userCodeMatch = document.cookie.match(/userCode[=:]([^;]+)/);
+            if (userCodeMatch) xh = userCodeMatch[1];
+        } catch (e) {}
+        
+        if (!xh) {
+            xh = await window.shiguangBridgePromise.showPrompt(
+                "输入学号",
+                "请输入你的学号:",
+                "",
+                null
+            );
+            if (!xh) return [];
+        }
+        
+        console.log("使用参数:", { xn, xq, xh });
+        
+        const paramStr = `xn=${xn}&xq=${xq}&xh=${xh}`;
+        const encodedParams = encodeParams(paramStr);
+        
+        const url = `https://jwxt.sxdzkj.edu.cn/wsxk/xkjg.ckdgxsxdkchj_data10319.jsp?params=${encodedParams}`;
+        
+        console.log("请求课表数据:", url);
+        
+        const response = await fetch(url, {
+            method: "GET",
+            credentials: "include"
+        });
+        
+        if (!response.ok) {
+            throw new Error(`请求失败: ${response.status}`);
+        }
+        
+        const arrayBuffer = await response.arrayBuffer();
+        let text;
+        try {
+            text = new TextDecoder('gbk').decode(arrayBuffer);
+        } catch (e) {
+            text = new TextDecoder('utf-8').decode(arrayBuffer);
+        }
+        
+        console.log("课表响应长度:", text.length);
+        
+        return parseCourseData(text);
+    } catch (error) {
+        window.shiguangBridge.showToast("获取课表失败: " + error.message);
+        console.error("获取课表失败:", error);
+        return [];
+    }
+}
+
+async function importPresetTimeSlots() {
+    const slots = [
+        { "number": 1, "startTime": "08:00", "endTime": "08:50" },
+        { "number": 2, "startTime": "09:00", "endTime": "09:50" },
+        { "number": 3, "startTime": "10:10", "endTime": "11:00" },
+        { "number": 4, "startTime": "11:10", "endTime": "12:00" },
+        { "number": 5, "startTime": "14:00", "endTime": "14:50" },
+        { "number": 6, "startTime": "15:00", "endTime": "15:50" },
+        { "number": 7, "startTime": "16:10", "endTime": "17:00" },
+        { "number": 8, "startTime": "17:10", "endTime": "18:00" },
+        { "number": 9, "startTime": "19:00", "endTime": "19:50" },
+        { "number": 10, "startTime": "20:00", "endTime": "20:50" }
+    ];
+    
+    try {
+        await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(slots));
+        window.shiguangBridge.showToast("时间段导入成功");
+    } catch (error) {
+        console.error("导入时间段失败:", error);
+    }
+}
+
+async function runImportFlow() {
+    const isReady = await checkLoginEnvironment();
+    if (!isReady) return;
+    
+    const confirmed = await window.shiguangBridgePromise.showAlert(
+        "教务导入",
+        "山西电子科技学院课表导入\n\n请确保已登录教务系统。\n点击确定开始导入。",
+        "确定导入"
+    );
+    if (!confirmed) return;
+    
+    const semesterParams = await getYearAndSemester();
+    if (!semesterParams) {
+        window.shiguangBridge.showToast("导入已取消");
+        return;
+    }
+    
+    console.log("选择的学期:", semesterParams);
+    
+    const courses = await fetchCourses(semesterParams.xn, semesterParams.xq);
+    if (!courses || courses.length === 0) {
+        window.shiguangBridge.showToast("未找到课程数据");
+        return;
+    }
+    
+    console.log("解析到的课程:", courses);
+    
+    try {
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        window.shiguangBridge.showToast(`成功保存 ${courses.length} 门课程`);
+    } catch (error) {
+        window.shiguangBridge.showToast("保存课程失败: " + error.message);
+        return;
+    }
+    
+    await importPresetTimeSlots();
+    
+    window.shiguangBridge.showToast(`导入完成!共 ${courses.length} 条课程记录`);
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+runImportFlow();

+ 13 - 4
resources/SYSU/adapters.yaml

@@ -1,8 +1,17 @@
-adapters: 
+# resources/SYSU/adapters.yaml
+adapters:
   - adapter_id: "SYSU"
-    adapter_name: "中山大学" 
+    adapter_name: "中山大学"
     category: "BACHELOR_AND_ASSOCIATE"
-    asset_js_path: "sysu.js" 
+    asset_js_path: "sysu.js"
     import_url: "https://jwxt.sysu.edu.cn/"
     maintainer: "JXCZ"
-    description: "中山大学NetID版本教务系统课程表导入"
+    description: "中山大学NetID版本教务系统课程表导入"
+
+  - adapter_id: "SYSU_01"
+    adapter_name: "中山大学研究生教务课表导入"
+    category: "POSTGRADUATE"
+    asset_js_path: "sysu_01.js"
+    import_url: "https://cms.sysu.edu.cn/"
+    maintainer: "JR2714"
+    description: "中山大学研究生教务(cms.sysu.edu.cn)"

+ 356 - 0
resources/SYSU/sysu_01.js

@@ -0,0 +1,356 @@
+// 中山大学研究生教务系统拾光课表适配脚本
+// 适配系统:cms.sysu.edu.cn /mk/student-web/(研究生课表查询)
+// 直接调用课表查询接口 selectStudentClassTable 抓取课表(同源 fetch,自动携带登录会话),不解析页面 DOM
+
+(function () {
+    "use strict";
+
+    // ---------- 通用工具 ----------
+
+    function toast(message) {
+        if (window.shiguangBridge && window.shiguangBridge.showToast) {
+            window.shiguangBridge.showToast(message);
+        } else {
+            console.log("[SYSU]", message);
+        }
+    }
+
+    async function alertUser(title, message) {
+        if (window.shiguangBridgePromise && window.shiguangBridgePromise.showAlert) {
+            return await window.shiguangBridgePromise.showAlert(title, message, "确定");
+        }
+        alert(title + "\n" + message);
+        return true;
+    }
+
+    // ---------- 作息时间(中山大学研究生, 11 节) ----------
+
+    const TIME_SLOTS = [
+        { number: 1, startTime: "08:00", endTime: "08:45" },
+        { number: 2, startTime: "08:55", endTime: "09:40" },
+        { number: 3, startTime: "10:10", endTime: "10:55" },
+        { number: 4, startTime: "11:05", endTime: "11:50" },
+        { number: 5, startTime: "14:20", endTime: "15:05" },
+        { number: 6, startTime: "15:15", endTime: "16:00" },
+        { number: 7, startTime: "16:30", endTime: "17:15" },
+        { number: 8, startTime: "17:25", endTime: "18:10" },
+        { number: 9, startTime: "19:00", endTime: "19:45" },
+        { number: 10, startTime: "19:55", endTime: "20:40" },
+        { number: 11, startTime: "20:50", endTime: "21:35" }
+    ];
+
+    // ================================================================
+    // 主路径: 接口直取
+    // ================================================================
+
+    const SCHEDULE_API = "/start-class/classTableInfo/selectStudentClassTable";
+    const CALENDAR_API = "/base-info/school-calender"; // 校历
+    const MENU_CODE = "byytxsd_xskbcx_week_query"; // 学生课表查询菜单 code
+
+    // 响应 JSON 里的星期字段名 -> 拾光 day(1=周一..7=周日)
+    const DAY_KEYS = {
+        monday: 1, tuesday: 2, wednesday: 3, thursday: 4,
+        friday: 5, saturday: 6, sunday: 7
+    };
+
+    /** 从当前页面自动探测学年学期, 如 "2026-1"; 找不到返回 null */
+    function detectAcademicYear() {
+        // 1) URL 参数
+        const urlMatch = location.href.match(/academicYear=([^&]+)/);
+        if (urlMatch) return decodeURIComponent(urlMatch[1]);
+        // 2) 页面标题, 如 <p class="title">2026-1学期课程</p>
+        const text = document.body ? document.body.innerText : "";
+        const titleMatch = text.match(/(\d{4}-\d)\s*学期/);
+        if (titleMatch) return titleMatch[1];
+        // 3) 页面上的学年学期下拉框(已选中项)
+        const selects = document.querySelectorAll("select");
+        for (let i = 0; i < selects.length; i++) {
+            const opt = selects[i].selectedOptions && selects[i].selectedOptions[0];
+            if (opt) {
+                const m = opt.value.match(/^\d{4}-\d$/);
+                if (m) return opt.value;
+            }
+        }
+        // 4) 页面任意文本中形如 2026-1 / 2026-2027-1 的学期串
+        const anyMatch = text.match(/(20\d{2}-\d)(?![\d-])/);
+        if (anyMatch) return anyMatch[1];
+        return null;
+    }
+
+    /** 从当前日期推算学年候选列表(登录后系统默认停在上学期, 假期导课表时要用新学期) */
+    function buildSemesterCandidates(detected) {
+        let baseYear = null;
+        const dm = String(detected || "").match(/^(\d{4})-(\d)$/);
+        if (dm) {
+            baseYear = parseInt(dm[1], 10);
+        } else {
+            const now = new Date();
+            const year = now.getFullYear();
+            const month = now.getMonth() + 1; // 1..12
+            // 学年 Y 覆盖 当年9月 ~ 次年8月; 9月(含)前属于上学年
+            baseYear = month >= 9 ? year : year - 1;
+        }
+        const out = [];
+        for (let y = baseYear - 1; y <= baseYear + 1; y++) {
+            out.push(y + "-1");
+            out.push(y + "-2");
+        }
+        return out;
+    }
+
+    /** 弹出学期选择, 返回选中的学年学期; 取消返回 null */
+    async function selectSemester(detected) {
+        const candidates = buildSemesterCandidates(detected);
+        let defaultIndex = 0;
+        if (detected) {
+            const idx = candidates.indexOf(detected);
+            if (idx >= 0) defaultIndex = idx;
+            else candidates.unshift(detected);
+        }
+        const selected = await window.shiguangBridgePromise.showSingleSelection(
+            "选择学年学期",
+            JSON.stringify(candidates),
+            defaultIndex
+        );
+        if (selected === null || selected === undefined) return null;
+        return candidates[parseInt(selected, 10)];
+    }
+
+    async function fetchSchedule(academicYear) {
+        const url = SCHEDULE_API +
+            "?code=" + MENU_CODE +
+            "&academicYear=" + encodeURIComponent(academicYear) +
+            "&weekly=0&_t=" + Date.now();
+        const resp = await fetch(url, {
+            method: "GET",
+            credentials: "include",
+            headers: { "X-Requested-With": "XMLHttpRequest" }
+        });
+        if (!resp.ok) throw new Error("接口请求失败: HTTP " + resp.status);
+        const json = await resp.json();
+        if (json.code !== 200) {
+            throw new Error("接口返回异常: code=" + json.code + (json.message ? " " + json.message : ""));
+        }
+        if (!Array.isArray(json.data)) {
+            throw new Error("接口返回的 data 不是数组");
+        }
+        return json.data;
+    }
+
+    /** 拉第 1 周周一作为开学日 */
+    async function fetchSemesterStartDate(academicYear) {
+        const url = CALENDAR_API +
+            "?academicYear=" + encodeURIComponent(academicYear) +
+            "&weekly=1&_t=" + Date.now();
+        const resp = await fetch(url, {
+            method: "GET",
+            credentials: "include",
+            headers: { "X-Requested-With": "XMLHttpRequest" }
+        });
+        if (!resp.ok) throw new Error("HTTP " + resp.status);
+        const json = await resp.json();
+        const start = json && json.data && json.data.startTime;
+        if (json.code !== 200 || !start || !/^\d{4}-\d{2}-\d{2}$/.test(String(start))) {
+            throw new Error("校历未返回有效 startTime");
+        }
+        return String(start);
+    }
+
+    /**
+     * 解析接口返回的字段串。
+     * 接口每个星期字段(如 tuesday)的值是若干 "键:值" 对用 ";;" 连接的字符串,
+     * 例如: "kcmc:课程名;;rkjs:教师;;skdd:地点;;zs:1-12周;;js:周二 2-4节;;..."。
+     * 本函数按 ";;" 切分后提取各键值, 返回 { name, teacher, position, weeks, startSection, endSection }。
+     */
+    function parseCourseField(fieldText) {
+        const fields = {};
+        String(fieldText).split(";;").forEach(function (part) {
+            const idx = part.indexOf(":");
+            if (idx <= 0) return;
+            fields[part.slice(0, idx).trim()] = part.slice(idx + 1).trim();
+        });
+
+        const name = fields.kcmc;
+        if (!name) return null;
+
+        // 周次范围: 取 zs(如 "1-12周"); 个别学期 zs 缺失时回退到 sksj(如 "1-12每周(2-4节)")
+        // 中同样的 "x-y周" 片段。
+        const zsText = String(fields.zs || fields.sksj || "");
+        const zsMatch = zsText.match(/(\d+)\s*-\s*(\d+)\s*每?周/) || zsText.match(/(\d+)\s*每?周/);
+        if (!zsMatch) {
+            console.warn("[SYSU] 无法解析周次:", zsText || "(空)", "字段:", fieldText);
+            return null;
+        }
+        const weekStart = parseInt(zsMatch[1], 10);
+        const weekEnd = zsMatch[2] ? parseInt(zsMatch[2], 10) : weekStart;
+        const weeks = [];
+        for (let w = weekStart; w <= weekEnd; w++) weeks.push(w);
+        if (!weeks.length) {
+            console.warn("[SYSU] 周次区间为空:", zsText);
+            return null;
+        }
+
+        // 节次: js 形如 "周二 2-4节" 或 "周二 3节"; 星期由外层 JSON 键决定。
+        const jsText = String(fields.js || "");
+        const jsMatch = jsText.match(/(\d+)\s*-\s*(\d+)\s*节/) || jsText.match(/(\d+)\s*节/);
+        if (!jsMatch) {
+            console.warn("[SYSU] 无法解析节次:", jsText || "(空)", "字段:", fieldText);
+            return null;
+        }
+        const startSection = parseInt(jsMatch[1], 10);
+        const endSection = jsMatch[2] ? parseInt(jsMatch[2], 10) : startSection;
+        if (!startSection || !endSection || endSection < startSection) {
+            console.warn("[SYSU] 节次区间无效:", jsText);
+            return null;
+        }
+
+        return {
+            name: name,
+            teacher: fields.rkjs || "未知教师",
+            position: fields.skdd || "未知地点",
+            weeks: weeks,
+            startSection: startSection,
+            endSection: endSection
+        };
+    }
+
+    /** 遍历响应数组(每项 = 某周某节次的当日课程), 提取全部上课并去重 */
+    function buildCoursesFromApi(data) {
+        const meetings = [];
+        let maxWeek = 0;
+
+        data.forEach(function (entry) {
+            if (!entry) return;
+            if (entry.weekly > maxWeek) maxWeek = entry.weekly;
+            Object.keys(DAY_KEYS).forEach(function (dayKey) {
+                const field = entry[dayKey];
+                if (!field || typeof field !== "string" || field.length < 4) return;
+                const course = parseCourseField(field);
+                if (!course) return;
+                course.weeks.forEach(function (w) {
+                    if (w > maxWeek) maxWeek = w;
+                });
+                meetings.push({
+                    name: course.name,
+                    teacher: course.teacher,
+                    position: course.position,
+                    day: DAY_KEYS[dayKey],
+                    startSection: course.startSection,
+                    endSection: course.endSection,
+                    weeks: course.weeks
+                });
+            });
+        });
+
+        return { meetings: dedupe(meetings), maxWeek: maxWeek };
+    }
+
+    function dedupe(meetings) {
+        const seen = {};
+        const out = [];
+        meetings.forEach(function (m) {
+            const key = [m.day, m.startSection, m.endSection, m.name, m.weeks.join(","), m.teacher, m.position].join("|");
+            if (seen[key]) return;
+            seen[key] = true;
+            out.push(m);
+        });
+        return out.sort(function (a, b) {
+            return a.day - b.day || a.startSection - b.startSection;
+        });
+    }
+
+    // ---------- 保存 ----------
+
+    async function saveToApp(courses, timeSlots, maxWeek, startDate) {
+        const config = {
+            semesterTotalWeeks: maxWeek > 0 ? maxWeek : 17,
+            firstDayOfWeek: 1,
+            defaultClassDuration: 45,
+            defaultBreakDuration: 10
+        };
+        if (startDate) config.semesterStartDate = startDate;
+        if (window.shiguangBridgePromise && window.shiguangBridgePromise.saveCourseConfig) {
+            await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+        }
+        if (timeSlots.length && window.shiguangBridgePromise && window.shiguangBridgePromise.savePresetTimeSlots) {
+            await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+        }
+        if (window.shiguangBridgePromise && window.shiguangBridgePromise.saveImportedCourses) {
+            return await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        }
+        console.log("[SYSU] parsed courses:", JSON.stringify(courses, null, 2));
+        return true;
+    }
+
+    // ---------- 主流程 ----------
+
+    async function runImportFlow() {
+        try {
+            // 1. 确定学年学期: 自动探测 + 弹窗让用户确认/修改
+            //    (假期登录时系统默认是上学期, 必须允许用户选到目标学期)
+            let academicYear = detectAcademicYear();
+            if (window.shiguangBridgePromise && window.shiguangBridgePromise.showSingleSelection) {
+                const picked = await selectSemester(academicYear);
+                if (picked === null) return; // 用户取消
+                academicYear = picked;
+            } else if (!academicYear) {
+                // 无 bridge 且页面也识别不到学期: 无法继续
+                await alertUser("未识别到学年学期", "请先进入目标学期的「课表查询」页面再执行导入。");
+                return;
+            }
+
+            // 2. 接口直取(主路径)
+            let courses = null, maxWeek = 0;
+            try {
+                toast("正在从教务接口获取 " + academicYear + " 课表...");
+                const data = await fetchSchedule(academicYear);
+                const result = buildCoursesFromApi(data);
+                if (result.meetings.length) {
+                    courses = result.meetings;
+                    maxWeek = result.maxWeek;
+                    toast("接口获取成功, 一周共 " + courses.length + " 次上课");
+                } else {
+                    console.warn("[SYSU] 接口返回空数据: academicYear=" + academicYear);
+                }
+            } catch (e) {
+                console.warn("[SYSU] 接口获取失败:", e);
+            }
+
+            if (!courses || !courses.length) {
+                await alertUser(
+                    "未解析到课程",
+                    "所选学期 " + academicYear + " 没有返回课程。\n\n可能原因:\n" +
+                    "1. 该学期课程尚未排课/选课(假期导入新学期时常见, 可等开学前选课后重试);\n" +
+                    "2. 学期代码不对(请重新执行导入, 在选择学期时核对);\n" +
+                    "3. 登录状态失效或接口异常(请重新进入课表页面登录后再试; 若仍失败, 请向适配维护者反馈)。"
+                );
+                return;
+            }
+
+            // 3. 校历
+            let startDate = null;
+            try {
+                startDate = await fetchSemesterStartDate(academicYear);
+                toast("已获取开学日 " + startDate);
+            } catch (e) {
+                console.warn("[SYSU] 校历获取失败, 跳过开学日:", e);
+            }
+
+            // 4. 保存
+            const saved = await saveToApp(courses, TIME_SLOTS, maxWeek, startDate);
+            if (!saved) { toast("课程保存失败, 请重试"); return; }
+
+            toast("导入成功: " + courses.length + " 个课程时段" +
+                (startDate ? ", 开学日 " + startDate : "") +
+                ", 已同步作息时间");
+            if (window.shiguangBridge && window.shiguangBridge.notifyTaskCompletion) {
+                window.shiguangBridge.notifyTaskCompletion();
+            }
+        } catch (error) {
+            console.error("[SYSU] import failed:", error);
+            await alertUser("导入失败", error && error.message ? error.message : String(error));
+        }
+    }
+
+    runImportFlow();
+})();