ソースを参照

Merge pull request #656 from XingHeYuZhuan/pending

星河欲转 1 週間 前
コミット
afbdab2eb2

+ 5 - 1
index/root_index.yaml

@@ -992,4 +992,8 @@ schools:
     name: "安徽电气工程职业技术学院"
     initial: "A"
     resource_folder: "AEPU"
-    
+    
+  - id: "SUSTECH"
+    name: "南方科技大学"
+    initial: "N"
+    resource_folder: "SUSTECH"

+ 1 - 1
resources/AEPU/adapters.yaml

@@ -3,6 +3,6 @@ adapters:
     adapter_name: "安徽电气工程职业技术学院教务系统"
     category: "BACHELOR_AND_ASSOCIATE"
     asset_js_path: "aepu.js"
-    import_url: "https://zt.aepu.com.cn/http/webvpn72eac4eb4dc8e2a039c8b6f3caab24955193a4ad25c9584e88a49b450bf0ce50/authserver/login?service=https%3A%2F%2Fzt.aepu.com.cn%2Fenlink%2Fapi%2Fclient%2Fcallback%2Fcas"
+    import_url: "https://zt.aepu.com.cn/http/webvpnae4cfe98f83bc8811cc5963452fd80627ab088b1bddf3f753580a56a75a714e9/ywtb-portal/official/index.html#/hall"
     maintainer: "Haooz"
     description: "安徽电气工程职业技术学院教务系统课表导入,请先登录并进入“我的课表”页面,再点击导入!"

+ 8 - 0
resources/SUSTECH/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "SUSTECH"
+    adapter_name: "南方科技大学教学管理与服务平台"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "sustech.js"
+    import_url: "https://cas.sustech.edu.cn/cas/login?service=https%3A%2F%2Ftis.sustech.edu.cn%2Fcas"
+    maintainer: "yusancky"
+    description: "南方科技大学 TIS 教学管理与服务平台(tis.sustech.edu.cn),需先通过 CAS 登录,登录后可直接导入;移动端需切换为电脑模式访问教务系统"

+ 790 - 0
resources/SUSTECH/sustech.js

@@ -0,0 +1,790 @@
+// 南方科技大学 TIS 教学管理与服务平台 课表导入适配器
+//
+// 依赖的 TIS 接口(均为登录后同域请求,凭证由 WebView 自动携带):
+//   POST /component/querydangqianxnxq        当前学年学期 { XN, XQ, XNXQ }
+//   POST /cjgl/grcjcx/grcjcx                 历年课程记录(用于生成可选学期列表)
+//   POST /xszykb/queryxszykbzong             整学期课表(xn, xq)
+//   POST /component/queryKbjg                作息时间(xn, xq, zc)
+//   POST /component/querydangqianzc          当前教学周(纯数字文本)
+//
+// 课表条目关键字段(2026 秋实测):
+//   KEY   "xq1_jc5"  -> 星期 1(周一),jc 为课表网格行号(非节次)
+//   KSJC / JSJC      起始 / 结束节次(真正的节次)
+//   ZC               0/1 位图,下标即周次(下标 0 未使用)
+//   SKSJ             多行文本:课程名 / [教师] / [班级] / [周次][地点][节次]
+//   KCWZSM / SKFS 等 实测为 null,仅作兜底
+
+// ========================================
+// 常量
+// ========================================
+
+// 教务系统未返回作息时间时的兜底节次时间(南科大常规作息)
+const FALLBACK_TIME_SLOTS = [
+    { number: 1, startTime: "08:00", endTime: "08:50" },
+    { number: 2, startTime: "09:00", endTime: "09:50" },
+    { number: 3, startTime: "10:20", endTime: "11:10" },
+    { number: 4, startTime: "11:20", endTime: "12:10" },
+    { number: 5, startTime: "14:00", endTime: "14:50" },
+    { number: 6, startTime: "15:00", endTime: "15:50" },
+    { number: 7, startTime: "16:20", endTime: "17:10" },
+    { number: 8, startTime: "17:20", endTime: "18:10" },
+    { number: 9, startTime: "19:00", endTime: "19:50" },
+    { number: 10, startTime: "20:00", endTime: "20:50" },
+    { number: 11, startTime: "21:20", endTime: "22:10" },
+    { number: 12, startTime: "22:20", endTime: "23:10" }
+];
+
+const CONFIG_BASE = {
+    semesterTotalWeeks: 18,
+    defaultClassDuration: 50,
+    defaultBreakDuration: 10
+};
+
+// ========================================
+// 网络请求
+// ========================================
+
+async function postFormRaw(path, params) {
+    const body = new URLSearchParams();
+
+    Object.keys(params || {}).forEach(function (key) {
+        const value = params[key];
+        body.append(key, value === null || value === undefined ? "" : String(value));
+    });
+
+    const response = await fetch(path, {
+        method: "POST",
+        credentials: "include",
+        headers: {
+            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+            "X-Requested-With": "XMLHttpRequest"
+        },
+        body: body.toString()
+    });
+
+    if (!response.ok) {
+        throw new Error(`请求 ${path} 失败:HTTP ${response.status}`);
+    }
+
+    return await response.text();
+}
+
+async function postForm(path, params) {
+    const text = await postFormRaw(path, params);
+    return parseJsonSafely(text, path);
+}
+
+async function postJson(path, payload) {
+    const response = await fetch(path, {
+        method: "POST",
+        credentials: "include",
+        headers: {
+            "Content-Type": "application/json",
+            "X-Requested-With": "XMLHttpRequest"
+        },
+        body: JSON.stringify(payload || {})
+    });
+
+    if (!response.ok) {
+        throw new Error(`请求 ${path} 失败:HTTP ${response.status}`);
+    }
+
+    return parseJsonSafely(await response.text(), path);
+}
+
+function parseJsonSafely(text, path) {
+    const trimmed = (text || "").trim();
+
+    if (!trimmed) {
+        return null;
+    }
+
+    if (trimmed.charAt(0) === "<") {
+        throw new Error("登录状态已失效,请重新登录本科教务系统后再导入。");
+    }
+
+    // 会话失效时 TIS 会返回登录提示页(HTML 或带"登录"字样的 JSON)
+    if (/重新登录|请登录|用户认证|登录已过期/.test(trimmed)) {
+        throw new Error("登录状态已失效,请重新登录教务系统后再导入。");
+    }
+
+    try {
+        return JSON.parse(trimmed);
+    } catch (error) {
+        throw new Error(
+            `教务系统返回了非 JSON 内容,请确认已登录本科教务系统(${path})。`
+        );
+    }
+}
+
+// 取数组型数据:接口可能直接返回数组,也可能包在 content / data 里
+function toArray(data) {
+    if (Array.isArray(data)) {
+        return data;
+    }
+
+    if (data && Array.isArray(data.content)) {
+        return data.content;
+    }
+
+    if (data && Array.isArray(data.data)) {
+        return data.data;
+    }
+
+    return [];
+}
+
+// ========================================
+// 学期信息
+// ========================================
+
+async function fetchCurrentSemester() {
+    const data = await postForm("/component/querydangqianxnxq", {});
+
+    if (!data || !data.XN || !data.XQ) {
+        throw new Error("未能获取学年学期,请确认已登录本科教务系统。");
+    }
+
+    return {
+        xn: String(data.XN),
+        xq: String(data.XQ),
+        label: data.XNXQ || `${data.XN}学年第${data.XQ}学期`
+    };
+}
+
+// 通过成绩/选课接口拿到该学生有记录的所有学期,作为可选列表
+async function fetchSemesterOptions() {
+    const options = [];
+    const seen = {};
+
+    try {
+        const data = await postJson("/cjgl/grcjcx/grcjcx", {
+            xn: null,
+            xq: null,
+            kcmc: null,
+            cxbj: "-1",
+            pylx: "1",
+            current: 1,
+            pageSize: 500
+        });
+
+        const rows = (data && data.content && data.content.list) || [];
+
+        rows.forEach(function (row) {
+            addSemesterOption(options, seen, row && row.xnxq, row && row.xnxqmc);
+        });
+    } catch (error) {
+        // 拿不到历史学期列表时不阻塞流程,调用方会退回"仅当前学期"
+        return options;
+    }
+
+    return options;
+}
+
+function addSemesterOption(options, seen, xnxq, label) {
+    const matched = String(xnxq || "").match(/^(\d{4}-\d{4})(\d)$/);
+
+    if (!matched) {
+        return;
+    }
+
+    const xn = matched[1];
+    const xq = matched[2];
+    const key = `${xn}-${xq}`;
+
+    if (seen[key]) {
+        return;
+    }
+
+    seen[key] = true;
+    options.push({ xn: xn, xq: xq, key: key, label: label || key });
+}
+
+function mergeCurrentSemester(options, current) {
+    const key = `${current.xn}-${current.xq}`;
+    const exists = options.some(function (item) {
+        return item.key === key;
+    });
+
+    if (!exists) {
+        options.push({
+            xn: current.xn,
+            xq: current.xq,
+            key: key,
+            label: current.label
+        });
+    }
+
+    options.sort(function (a, b) {
+        return b.key < a.key ? -1 : 1;
+    });
+
+    return options;
+}
+
+// ========================================
+// 用户交互
+// ========================================
+
+function validateDateInput(input) {
+    const text = String(input || "").trim();
+
+    if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) {
+        return "请输入 YYYY-MM-DD 格式的日期!";
+    }
+
+    const date = new Date(`${text}T00:00:00`);
+
+    if (isNaN(date.getTime())) {
+        return "请输入有效的日期!";
+    }
+
+    return false;
+}
+
+async function confirmStart() {
+    return await window.shiguangBridgePromise.showAlert(
+        "导入说明",
+        "本适配用于导入南方科技大学 TIS 本科教务系统的课表。\n请先完成 CAS 登录并停留在教务系统页面内,再点击确认开始导入。",
+        "开始导入"
+    );
+}
+
+async function selectSemester(options, current) {
+    if (!options.length) {
+        return { xn: current.xn, xq: current.xq, key: `${current.xn}-${current.xq}` };
+    }
+
+    const labels = options.map(function (item) {
+        return `${item.label}(${item.xn}学年第${item.xq}学期)`;
+    });
+
+    const defaultIndex = options.findIndex(function (item) {
+        return item.key === `${current.xn}-${current.xq}`;
+    });
+
+    const index = await window.shiguangBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(labels),
+        defaultIndex >= 0 ? defaultIndex : 0
+    );
+
+    if (index === null || index < 0 || index >= options.length) {
+        return null;
+    }
+
+    return options[index];
+}
+
+async function askSemesterStartDate(defaultValue) {
+    const input = await window.shiguangBridgePromise.showPrompt(
+        "学期开始日期",
+        "该学期不是当前学期,无法自动推算周次。\n请输入该学期第 1 周星期一的日期:",
+        defaultValue,
+        "validateDateInput"
+    );
+
+    if (input === null) {
+        return null;
+    }
+
+    return String(input).trim();
+}
+
+// ========================================
+// 课表数据与作息时间
+// ========================================
+
+async function fetchTimeSlots(xn, xq) {
+    try {
+        const data = await postForm("/component/queryKbjg", { xn: xn, xq: xq, zc: "1" });
+        const slots = toArray(data).map(function (item) {
+            const number = toNumber(item.xj);
+            const startTime = normalizeTime(item.kssj);
+            const endTime = normalizeTime(item.jssj);
+
+            if (!number || !startTime || !endTime) {
+                return null;
+            }
+
+            return { number: number, startTime: startTime, endTime: endTime };
+        }).filter(Boolean);
+
+        if (slots.length) {
+            slots.sort(function (a, b) {
+                return a.number - b.number;
+            });
+            return slots;
+        }
+    } catch (error) {
+        // 忽略,使用兜底作息
+    }
+
+    return FALLBACK_TIME_SLOTS.slice();
+}
+
+async function fetchCourses(xn, xq) {
+    const data = await postForm("/xszykb/queryxszykbzong", { xn: xn, xq: xq });
+    const rows = toArray(data);
+
+    if (!rows.length) {
+        throw new Error("教务系统未返回课表数据,请确认该学期已选课。");
+    }
+
+    return parseCourses(rows);
+}
+
+function parseCourses(rows) {
+    const courses = [];
+    const seen = {};
+
+    rows.forEach(function (row) {
+        const course = parseCourse(row);
+
+        if (!course) {
+            return;
+        }
+
+        const identity = [
+            course.name,
+            course.day,
+            course.startSection,
+            course.endSection,
+            course.weeks.join(","),
+            course.teacher,
+            course.position
+        ].join("|");
+
+        // 同一节课会在课表网格中占多行(KEY 的 jc 不同),需要去重
+        if (seen[identity]) {
+            return;
+        }
+
+        seen[identity] = true;
+        courses.push(course);
+    });
+
+    return courses;
+}
+
+function parseCourse(row) {
+    if (!row || typeof row !== "object") {
+        return null;
+    }
+
+    const keyMatch = String(row.KEY || "").match(/xq(\d+)_jc(\d+)/i);
+    const day = keyMatch ? toNumber(keyMatch[1]) : null;
+
+    if (!day || day < 1 || day > 7) {
+        return null;
+    }
+
+    const detail = parseScheduleText(row.SKSJ || row.SKSJ_EN);
+
+    // 真实数据中 KCWZSM 为 null,课程信息以 SKSJ 为准
+    const name = firstText(detail.name, row.KCMC, row.KCWZSM);
+
+    if (!name) {
+        return null;
+    }
+
+    // jc 只是网格行号,真实节次用 KSJC / JSJC
+    const slotIndex = keyMatch ? toNumber(keyMatch[2]) : null;
+    const startSection = toNumber(row.KSJC) || slotIndex;
+    const endSection = toNumber(row.JSJC) || startSection;
+    const weeks = parseWeekBitmap(row.ZC, detail.declaredWeeks);
+
+    if (!startSection || !endSection || endSection < startSection || !weeks.length) {
+        return null;
+    }
+
+    return {
+        name: name,
+        teacher: firstText(detail.teacher, row.SKJS, row.JSXM, row.DGJSMC),
+        position: firstText(detail.position, row.SKDD, row.JXCDMC, row.SKDDMC),
+        day: day,
+        startSection: startSection,
+        endSection: endSection,
+        weeks: weeks
+    };
+}
+
+// ZC 是 0/1 位图,实测下标即周次(下标 0 未使用)。
+// 为兼容不同学期的差异,同时算出两种解释,用 SKSJ 里声明的周次(如 [1-16周])校准。
+function parseWeekBitmap(value, declaredWeeks) {
+    const bitmap = String(value || "");
+
+    if (!/^[01]+$/.test(bitmap)) {
+        return declaredWeeks || [];
+    }
+
+    function build(shift) {
+        const weeks = [];
+
+        for (let index = 0; index < bitmap.length; index += 1) {
+            if (bitmap[index] === "1") {
+                const week = index + 1 - shift;
+
+                if (week > 0) {
+                    weeks.push(week);
+                }
+            }
+        }
+
+        return weeks;
+    }
+
+    // shift=1:周次 = 下标(教务实际使用的口径);shift=0:周次 = 下标 + 1
+    const candidates = [build(1), build(0)];
+
+    if (!declaredWeeks || !declaredWeeks.length) {
+        return candidates[0];
+    }
+
+    let best = candidates[0];
+    let bestScore = -1;
+
+    candidates.forEach(function (weeks) {
+        const set = {};
+        weeks.forEach(function (week) {
+            set[week] = true;
+        });
+
+        const score = declaredWeeks.filter(function (week) {
+            return set[week];
+        }).length;
+
+        if (score > bestScore) {
+            bestScore = score;
+            best = weeks;
+        }
+    });
+
+    return best;
+}
+
+const WEEK_PATTERN = /\d[\d,\-]*(单|双)?周/;      // 1-16周 / 1-15单周 / 1-9,11周
+const SECTION_PATTERN = /\d+\s*(-\s*\d+)?\s*节/;  // 9-10节 / 3节
+
+// 详情行:含周次或节次的那些行,如 [1-16周][一教110][1-2节]、[3-16周]、[教室][1-4节]
+function isDetailLine(line) {
+    return WEEK_PATTERN.test(line) || SECTION_PATTERN.test(line);
+}
+
+// 解析 "1-16周" / "1-9,11,13-15周" / "1-15单周" 形式的周次声明
+function parseWeekExpression(text) {
+    const matched = String(text).match(WEEK_PATTERN);
+
+    if (!matched) {
+        return [];
+    }
+
+    // 去掉末尾的 "单周" / "双周" / "周",剩下的就是周次表达式
+    const expression = matched[0].replace(/(单|双)?周$/, "");
+    const parity = /单周$/.test(matched[0]) ? "单" : (/双周$/.test(matched[0]) ? "双" : "");
+    const weeks = [];
+
+    expression.split(",").forEach(function (part) {
+        const range = part.match(/^(\d+)(?:-(\d+))?$/);
+
+        if (!range) {
+            return;
+        }
+
+        const start = Number(range[1]);
+        const end = range[2] ? Number(range[2]) : start;
+
+        for (let week = start; week <= end && week <= 36; week += 1) {
+            weeks.push(week);
+        }
+    });
+
+    const filtered = weeks.filter(function (week) {
+        if (parity === "单") {
+            return week % 2 === 1;
+        }
+
+        if (parity === "双") {
+            return week % 2 === 0;
+        }
+
+        return true;
+    });
+
+    return uniqueSorted(filtered);
+}
+
+// SKSJ 形如:
+//   思想道德与法治
+//   [兰美荣]
+//   [思想道德与法治-09班-中文]
+//   [1-16周][智华楼207][9-10节]
+function parseScheduleText(value) {
+    const lines = String(value || "")
+        .replace(/<br\s*\/?>/gi, "\n")
+        .replace(/<[^>]+>/g, "\n")
+        .split("\n")
+        .map(function (line) {
+            return line.trim();
+        })
+        .filter(Boolean);
+
+    const result = { name: "", teacher: "", position: "", declaredWeeks: [] };
+
+    if (!lines.length) {
+        return result;
+    }
+
+    result.name = unwrap(lines[0]);
+
+    lines.slice(1).forEach(function (line) {
+        const groups = bracketGroups(line);
+
+        // 详情行:[1-16周][智华楼207][9-10节]
+        // 注意:不能用 "含周/含节" 判断,课程名或教师名里可能有"周"字(如"某单周课")
+        if (isDetailLine(line)) {
+            if (!result.declaredWeeks.length) {
+                result.declaredWeeks = parseWeekExpression(line);
+            }
+
+            if (!result.position) {
+                const place = firstMatch(groups, function (part) {
+                    return part && !WEEK_PATTERN.test(part) && !SECTION_PATTERN.test(part) &&
+                        !/班/.test(part) && part !== result.name &&
+                        part.indexOf(result.name) < 0 && result.name.indexOf(part) < 0;
+                });
+
+                if (place) {
+                    result.position = unwrap(place);
+                }
+            }
+
+            return;
+        }
+
+        const text = unwrap(line);
+
+        // 教师行:课程名的下一行,排除班级行(含"班")与课程名本身
+        if (!result.teacher && text && text !== result.name && !/班/.test(text)) {
+            result.teacher = text;
+        }
+    });
+
+    return result;
+}
+
+// ========================================
+// 课表配置(学期开始日期 / 总周数)
+// ========================================
+
+async function fetchCurrentWeek() {
+    try {
+        const text = (await postFormRaw("/component/querydangqianzc", {})).trim();
+
+        if (/^\d+$/.test(text)) {
+            return Number(text);
+        }
+    } catch (error) {
+        // 学期未开始时该接口返回空内容
+    }
+
+    return null;
+}
+
+// 本周周一往前推 (当前周 - 1) 周,即为第 1 周周一
+function computeSemesterStart(currentWeek) {
+    if (!currentWeek || currentWeek < 1) {
+        return "";
+    }
+
+    const now = new Date();
+    const monday = new Date(
+        now.getFullYear(),
+        now.getMonth(),
+        now.getDate() - ((now.getDay() + 6) % 7)
+    );
+
+    monday.setDate(monday.getDate() - (currentWeek - 1) * 7);
+
+    return formatDate(monday);
+}
+
+async function buildCourseConfig(semester, current, courses) {
+    const maxWeek = courses.reduce(function (max, course) {
+        return Math.max(max, course.weeks[course.weeks.length - 1] || 0);
+    }, 0);
+
+    const config = Object.assign({}, CONFIG_BASE, {
+        semesterTotalWeeks: Math.max(CONFIG_BASE.semesterTotalWeeks, maxWeek)
+    });
+
+    const isCurrent = semester && current && semester.key === `${current.xn}-${current.xq}`;
+    let startDate = isCurrent ? computeSemesterStart(await fetchCurrentWeek()) : "";
+
+    if (!startDate) {
+        startDate = await askSemesterStartDate(computeSemesterStart(1));
+
+        if (!startDate) {
+            return null;
+        }
+    }
+
+    config.semesterStartDate = startDate;
+
+    return config;
+}
+
+// ========================================
+// 工具函数
+// ========================================
+
+function firstText() {
+    for (let index = 0; index < arguments.length; index += 1) {
+        const value = arguments[index];
+
+        if (value !== null && value !== undefined && String(value).trim() !== "") {
+            return String(value).trim();
+        }
+    }
+
+    return "";
+}
+
+// 去掉一对包裹的中括号:[兰美荣] -> 兰美荣
+function unwrap(value) {
+    return String(value || "").trim()
+        .replace(/^\[/, "")
+        .replace(/\]$/, "")
+        .trim();
+}
+
+// 提取一行里所有 [xxx] 的内容
+function bracketGroups(line) {
+    const groups = [];
+    const pattern = /\[([^\]]*)\]/g;
+    let matched = pattern.exec(String(line));
+
+    while (matched !== null) {
+        groups.push(matched[1].trim());
+        matched = pattern.exec(String(line));
+    }
+
+    return groups;
+}
+
+function firstMatch(list, predicate) {
+    for (let index = 0; index < list.length; index += 1) {
+        if (predicate(list[index])) {
+            return list[index];
+        }
+    }
+
+    return null;
+}
+
+function uniqueSorted(numbers) {
+    const seen = {};
+    const result = [];
+
+    numbers.forEach(function (number) {
+        if (!seen[number]) {
+            seen[number] = true;
+            result.push(number);
+        }
+    });
+
+    return result.sort(function (a, b) {
+        return a - b;
+    });
+}
+
+function toNumber(value) {
+    if (value === null || value === undefined || value === "") {
+        return null;
+    }
+
+    const number = Number(value);
+
+    return Number.isFinite(number) && number > 0 ? number : null;
+}
+
+function normalizeTime(value) {
+    const text = String(value || "").trim();
+
+    return /^\d{1,2}:\d{2}$/.test(text) ? text : "";
+}
+
+function formatDate(date) {
+    const month = String(date.getMonth() + 1).padStart(2, "0");
+    const day = String(date.getDate()).padStart(2, "0");
+
+    return `${date.getFullYear()}-${month}-${day}`;
+}
+
+function toast(message) {
+    if (window.shiguangBridge && typeof window.shiguangBridge.showToast === "function") {
+        window.shiguangBridge.showToast(message);
+    }
+}
+
+// ========================================
+// 主流程
+// ========================================
+
+async function runImportFlow() {
+    try {
+        if (!window.shiguangBridgePromise) {
+            throw new Error("当前环境不支持导入,请在时光课程表 App 内运行。");
+        }
+
+        const confirmed = await confirmStart();
+
+        if (!confirmed) {
+            toast("已取消导入。");
+            return;
+        }
+
+        toast("正在获取学期信息...");
+
+        const current = await fetchCurrentSemester();
+        const options = mergeCurrentSemester(await fetchSemesterOptions(), current);
+        const semester = await selectSemester(options, current);
+
+        if (!semester) {
+            toast("已取消导入。");
+            return;
+        }
+
+        toast("正在获取课表...");
+
+        const courses = await fetchCourses(semester.xn, semester.xq);
+
+        if (!courses.length) {
+            throw new Error("未解析到任何课程,请确认该学期课表已发布。");
+        }
+
+        const timeSlots = await fetchTimeSlots(semester.xn, semester.xq);
+        const config = await buildCourseConfig(semester, current, courses);
+
+        if (!config) {
+            toast("已取消导入。");
+            return;
+        }
+
+        toast(`正在导入 ${courses.length} 门课程...`);
+
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+
+        toast(`成功导入 ${courses.length} 门课程!`);
+
+        if (window.shiguangBridge && typeof window.shiguangBridge.notifyTaskCompletion === "function") {
+            window.shiguangBridge.notifyTaskCompletion();
+        }
+    } catch (error) {
+        toast(`导入失败:${error.message}`);
+    }
+}
+
+runImportFlow();