/** * 长江大学(树维教务系统)课表导入适配脚本 * * 树维教务系统特点: * 1. 课表以空 HTML 表格返回,课程数据通过 JavaScript 脚本动态注入 * 2. 脚本中包含 `new TaskActivity(...)` 构造函数调用来定义课程 * 3. 需要从脚本文本中直接提取课程信息,而不是解析 DOM * * 适用于使用树维教务系统的其他高校(需修改 BASE 地址) */ (function () { const BASE = "https://jwc3-yangtzeu-edu-cn-s.atrust.yangtzeu.edu.cn"; const MAX_PREVIEW_LEN = 300; const diagState = { currentStep: "init", events: [] }; function truncateText(value, maxLen) { const text = String(value == null ? "" : value); if (text.length <= maxLen) return text; return `${text.slice(0, maxLen)}...`; } function toSafeJson(value) { try { return JSON.stringify(value); } catch (_) { return String(value); } } function recordDiag(step, info) { diagState.currentStep = step || diagState.currentStep; diagState.events.push({ at: new Date().toISOString(), step: diagState.currentStep, info: info || "" }); if (diagState.events.length > 80) { diagState.events = diagState.events.slice(-80); } } function createImportError(step, message, context, cause) { const error = new Error(message || "导入失败"); error.name = "ImportFlowError"; error.step = step || diagState.currentStep; error.context = context || {}; error.cause = cause; return error; } function formatErrorDetails(error) { const lines = []; const err = error || {}; const step = err.step || diagState.currentStep || "unknown"; const now = new Date().toISOString(); lines.push(`Time: ${now}`); lines.push(`Step: ${step}`); lines.push(`Name: ${err.name || "Error"}`); lines.push(`Message: ${err.message || String(err)}`); if (err.stack) { lines.push("Stack:"); lines.push(String(err.stack)); } if (err.context && Object.keys(err.context).length > 0) { lines.push("Context:"); lines.push(truncateText(toSafeJson(err.context), 1500)); } if (err.cause) { const causeMsg = err.cause && err.cause.message ? err.cause.message : String(err.cause); lines.push(`Cause: ${causeMsg}`); if (err.cause && err.cause.stack) { lines.push("CauseStack:"); lines.push(String(err.cause.stack)); } } if (diagState.events.length > 0) { lines.push("Trace:"); const recentEvents = diagState.events.slice(-20); recentEvents.forEach((event) => { lines.push(`[${event.at}] ${event.step} | ${truncateText(event.info, 200)}`); }); } return lines.join("\n"); } function extractCourseHtmlDebugInfo(courseHtml) { const text = String(courseHtml || ""); const hasTaskActivity = /new\s+TaskActivity\s*\(/i.test(text); const hasUnitCount = /\bvar\s+unitCount\s*=\s*\d+/i.test(text); return { responseLength: text.length, hasTaskActivity, hasUnitCount, headPreview: truncateText(text.slice(0, 2000), 2000), tailPreview: truncateText(text.slice(-1000), 1000) }; } function safeToast(message) { try { window.AndroidBridge && AndroidBridge.showToast(String(message || "")); } catch (_) { console.log("[Toast Fallback]", message); } } async function safeShowDetailedError(title, details) { const text = truncateText(details, 3500); try { if (window.AndroidBridgePromise && typeof window.AndroidBridgePromise.showAlert === "function") { await window.AndroidBridgePromise.showAlert(title || "导入失败", text, "确定"); return; } } catch (alertError) { console.warn("[Error Alert Fallback] showAlert failed:", alertError); } safeToast(title || "导入失败"); console.error("[Detailed Error]", text); } function ensureBridgePromise() { if (!window.AndroidBridgePromise) { throw new Error("AndroidBridgePromise 不可用,无法进行导入交互。"); } } async function requestText(url, options) { const requestOptions = { credentials: "include", ...options }; const method = requestOptions.method || "GET"; recordDiag("http_request", `${method} ${url}`); let res; try { res = await fetch(url, requestOptions); } catch (networkError) { throw createImportError( "http_request", `网络请求失败: ${method} ${url}`, { url, method, bodyPreview: truncateText(requestOptions.body, MAX_PREVIEW_LEN) }, networkError ); } const text = await res.text(); recordDiag("http_response", `${method} ${url} -> ${res.status}, len=${text.length}`); if (!res.ok) { throw createImportError("http_response", `请求失败(${res.status}): ${url}`, { url, method, status: res.status, bodyPreview: truncateText(requestOptions.body, MAX_PREVIEW_LEN), responsePreview: truncateText(text, MAX_PREVIEW_LEN) }); } return text; } // 从入口页面 HTML 中提取学生 ID 和学期选择组件的 tagId // 树维系统通过 bg.form.addInput 注入学生 ID,通过 semesterBar 提供学期选择 function parseEntryParams(entryHtml) { const idsMatch = entryHtml.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/); const tagIdMatch = entryHtml.match(/id="(semesterBar\d+Semester)"/); return { studentId: idsMatch ? idsMatch[1] : "", tagId: tagIdMatch ? tagIdMatch[1] : "" }; } // 解析学期列表,树维接口返回的是 JavaScript 对象字面量(非标准 JSON) // 格式: { semesters: { "2024-2025-1": [{id: 389, schoolYear: "2024-2025", name: "1"}] } } function parseSemesterResponse(rawText) { let data; try { // 使用 Function 构造器执行对象字面量文本 data = Function(`return (${String(rawText || "").trim()});`)(); } catch (parseError) { throw createImportError( "parse_semester", "学期数据解析失败", { rawPreview: truncateText(rawText, MAX_PREVIEW_LEN) }, parseError ); } const semesters = []; if (!data || !data.semesters || typeof data.semesters !== "object") { return semesters; } Object.keys(data.semesters).forEach((k) => { const arr = data.semesters[k]; if (!Array.isArray(arr)) return; arr.forEach((s) => { if (!s || !s.id) return; semesters.push({ id: String(s.id), name: `${s.schoolYear || ""} 第${s.name || ""}学期`.trim() }); }); }); return semesters; } // 清除课程名后面的课程序号 function cleanCourseName(name) { return String(name || "").replace(/\(\d+\)\s*$/, "").trim(); } // 解析周次位图字符串,树维系统使用位图表示课程在哪些周有效 function parseValidWeeksBitmap(bitmap) { if (!bitmap || typeof bitmap !== "string") return []; const weeks = []; for (let i = 0; i < bitmap.length; i++) { if (bitmap[i] === "1" && i >= 1) weeks.push(i); } return weeks; } function normalizeWeeks(weeks) { const list = Array.from(new Set((weeks || []).filter((w) => Number.isInteger(w) && w > 0))); list.sort((a, b) => a - b); return list; } // 将教务系统的节次映射到 TimeSlots 编号 // 教务系统返回的节次顺序: 1-6为正常排列,7为午间课,8为晚间课 // TimeSlots 的顺序: 完全按时间排列,3为午间课,6为晚间课 function mapSectionToTimeSlotNumber(section) { const mapping = { 1: 1, 2: 2, 3: 4, 4: 5, 5: 7, 6: 8, 7: 3, 8: 6 }; return mapping[section] || section; } // 反引号化 JavaScript 字面量字符串,处理转义字符 function unquoteJsLiteral(token) { const text = String(token || "").trim(); if (!text) return ""; if (text === "null" || text === "undefined") return ""; if ((text.startsWith("\"") && text.endsWith("\"")) || (text.startsWith("'") && text.endsWith("'"))) { const quote = text[0]; let inner = text.slice(1, -1); inner = inner .replace(/\\\\/g, "\\") .replace(new RegExp(`\\\\${quote}`, "g"), quote) .replace(/\\n/g, "\n") .replace(/\\r/g, "\r") .replace(/\\t/g, "\t"); return inner; } return text; } // 分割 JavaScript 函数参数字符串,正确处理引号和转义 function splitJsArgs(argsText) { const args = []; let curr = ""; let inQuote = ""; let escaped = false; for (let i = 0; i < argsText.length; i++) { const ch = argsText[i]; if (escaped) { curr += ch; escaped = false; continue; } if (ch === "\\") { curr += ch; escaped = true; continue; } if (inQuote) { curr += ch; if (ch === inQuote) inQuote = ""; continue; } if (ch === "\"" || ch === "'") { curr += ch; inQuote = ch; continue; } if (ch === ",") { args.push(curr.trim()); curr = ""; continue; } curr += ch; } if (curr.trim() || argsText.endsWith(",")) { args.push(curr.trim()); } return args; } /** * 从课表响应的 JavaScript 脚本中解析课程(树维教务核心解析逻辑) * * 树维系统返回的 HTML 中,表格单元格是空的,真正的课程数据在