hbuas_01.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. // 湖北文理学院(hbuas.edu.cn) 拾光课程表适配脚本
  2. // 教务系统:超星教务(hbuas.jw.chaoxing.com,dbname = hbuas)
  3. // 使用流程:登录教务系统(账密登录或学习通扫码均可)→ 停留在任意教务页面 → 点击“执行导入”
  4. //
  5. // 接口说明(2026-2027-1 学期实测,返回 ret 均为 0):
  6. // GET /admin/pkgl/xskb/queryKbForXsd 课表页 HTML(内含 #xhid / #xqdm / #xnxq1)
  7. // GET /admin/api/jcsj/xqsj/getXqList 校区列表(item.id = 校区代码,item.xqmc = 名称)
  8. // GET /admin/api/getZclistByXnxq?xnxq=<学年-学期>&xqid=<校区代码> 周次表 data.zclist + 节次时间 data.jcsjszList
  9. // GET /admin/xsd/pkgl/xskb/sdpkkbList?xnxq=&xhid=&xqdm=&xskbxslx=0 课程数据 JSON(data[])
  10. //
  11. // 细节说明:
  12. // 1. 课程数据按“单节”返回(djc = 第几节),脚本先逐节转成课程块,
  13. // 再交给 wiki 教程《课程合并与去重函数》中的合并去重函数处理:
  14. // 连续节次合并(1-2 + 3-4 → 1-4)、单双周合并(1,3,5 + 2,4,6 → 1-6)、完全重复去重。
  15. // 实测 60 条原始记录 → 26 条,节次×周次单元 472 → 472,无数据丢失。
  16. // 2. kcmc / tmc / croommc 都是 <a> 包裹的 HTML 片段,需要抽文本。
  17. // 3. 教务系统的 #xnxq1 会把“当前学期”标为 selected,脚本把它作为学期单选列表的默认项:
  18. // 常用场景一键确认;学期未开始时也可改选下学期提前导入。校区则默认沿用页面校区、不弹窗。
  19. // 4. 作息时间与开学日期都从教务接口读取,不写死;仅在接口异常时回退到内置的北校区作息表。
  20. //
  21. // 维护者:salt-fishes
  22. // 学期选择:默认【弹出单选列表】,并把教务系统标记的“当前学期”作为默认选中项。
  23. // 常用场景直接确认即可;学期尚未开始时,也可在这里改选下学期,提前导入课表。
  24. // 若希望完全不弹窗、始终使用当前学期,改为 false。
  25. const ASK_SEMESTER = true;
  26. // 校区选择:默认沿用当前页面校区、不弹窗(同一学生通常只在一个校区上课)。
  27. // 若希望每次手动选择校区,改为 true。
  28. const ASK_CAMPUS = false;
  29. // 接口异常时的兜底作息(2026-2027-1 北校区实测值),正常情况不会用到
  30. const FALLBACK_TIME_SLOTS = [
  31. { number: 1, startTime: "08:00", endTime: "08:45" },
  32. { number: 2, startTime: "08:55", endTime: "09:40" },
  33. { number: 3, startTime: "10:00", endTime: "10:45" },
  34. { number: 4, startTime: "10:50", endTime: "11:40" },
  35. { number: 5, startTime: "14:00", endTime: "14:45" },
  36. { number: 6, startTime: "14:55", endTime: "15:40" },
  37. { number: 7, startTime: "16:00", endTime: "16:45" },
  38. { number: 8, startTime: "16:45", endTime: "17:30" },
  39. { number: 9, startTime: "18:30", endTime: "19:15" },
  40. { number: 10, startTime: "19:15", endTime: "20:00" }
  41. ];
  42. // 兜底总周数(接口返回 zclist 长度时以接口为准)
  43. const FALLBACK_TOTAL_WEEKS = 20;
  44. // 出错时附在弹窗里的诊断信息(便于用户反馈时定位)
  45. let diagnoseInfo = '';
  46. // =========================================================================
  47. // 桥接封装
  48. // =========================================================================
  49. function toast(message) {
  50. if (window.shiguangBridge && typeof window.shiguangBridge.showToast === 'function') {
  51. window.shiguangBridge.showToast(message);
  52. } else {
  53. console.log('[HBUAS] ' + message);
  54. }
  55. }
  56. async function alertUser(title, content) {
  57. if (window.shiguangBridgePromise && typeof window.shiguangBridgePromise.showAlert === 'function') {
  58. return await window.shiguangBridgePromise.showAlert(title, content, "确定");
  59. }
  60. console.warn('[HBUAS] ' + title + ' - ' + content);
  61. return true;
  62. }
  63. // =========================================================================
  64. // 文本与周次解析
  65. // =========================================================================
  66. // 超星返回的字段带有 <a> 标签,取出其中的纯文本
  67. function extractAnchorText(htmlStr) {
  68. if (!htmlStr) return '';
  69. const match = String(htmlStr).match(/>([^<]+)</);
  70. return match ? match[1].trim() : String(htmlStr).trim();
  71. }
  72. // 去掉教师名里的括号备注
  73. function cleanTeacherName(name) {
  74. if (!name) return '';
  75. return name.replace(/([^)]*)/g, '').replace(/\([^)]*\)/g, '').trim();
  76. }
  77. // 超星的 zcstr 直接是逗号分隔的周次数字,例如 "1,2,3,10,11"
  78. function parseWeeks(weekStr) {
  79. if (!weekStr) return [];
  80. return String(weekStr).split(',')
  81. .map(w => Number(String(w).trim()))
  82. .filter(w => !isNaN(w) && w > 0)
  83. .sort((a, b) => a - b);
  84. }
  85. function formatTime(timeStr) {
  86. if (typeof timeStr !== 'string') return null;
  87. const match = timeStr.match(/^(\d{1,2}):(\d{1,2})(?::\d{1,2})?$/);
  88. if (!match) return null;
  89. const hour = Number(match[1]);
  90. const minute = Number(match[2]);
  91. return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
  92. }
  93. // =========================================================================
  94. // 网络请求封装
  95. // =========================================================================
  96. async function getJson(url) {
  97. const response = await fetch(url, {
  98. method: 'GET',
  99. credentials: 'include',
  100. headers: {
  101. 'Accept': 'application/json, text/plain, */*',
  102. 'X-Requested-With': 'XMLHttpRequest'
  103. }
  104. });
  105. if (!response.ok) throw new Error(`网络请求失败,状态码 ${response.status}(${url.split('?')[0]})`);
  106. const json = await response.json();
  107. if (json.ret !== 0) {
  108. // 带上接口名与 ret,便于用户反馈时快速定位(例如 xhid 无效时 sdpkkbList 会返回 ret=-1)
  109. throw new Error(`${url.split('?')[0]} 返回 ret=${json.ret}:${json.msg || '未知错误'}`);
  110. }
  111. return json;
  112. }
  113. // =========================================================================
  114. // 页面参数提取
  115. // =========================================================================
  116. // 课表页地址。其服务端渲染的 #xhid(86 字符加密串)/ #xqdm / #xnxq1 才是正确参数。
  117. // 注意:教务首页 /admin 里也存在一个 id="xhid" 的元素,但值是用户 ID(32 字符)。
  118. // 若误用它调 sdpkkbList,服务端会返回 ret=-1「功能暂时停用,请联系管理员」。
  119. // 因此这里【始终优先采用课表页里的值】,只有当课表页抓取失败时才退回当前页面 DOM。
  120. const KB_PAGE_URL = '/admin/pkgl/xskb/queryKbForXsd';
  121. function pickKbParams(doc) {
  122. return {
  123. xhid: doc.querySelector('#xhid')?.value || '',
  124. xqdm: doc.querySelector('#xqdm')?.value || '',
  125. xnxqSelectHtml: doc.querySelector('#xnxq1')?.outerHTML || null
  126. };
  127. }
  128. // 提取 #xhid(学号加密串)/ #xqdm(校区代码)/ #xnxq1(学期下拉)
  129. async function extractPageParams() {
  130. let pageHtml = null;
  131. try {
  132. const response = await fetch(KB_PAGE_URL, {
  133. method: 'GET',
  134. credentials: 'include',
  135. headers: { 'Accept': 'text/html,application/xhtml+xml,*/*;q=0.8' }
  136. });
  137. if (response.ok) {
  138. pageHtml = await response.text();
  139. } else {
  140. console.warn(`JS: 抓取课表页失败,状态码 ${response.status}`);
  141. }
  142. } catch (error) {
  143. console.warn('JS: 抓取课表页异常:', error.message);
  144. }
  145. let params = { xhid: '', xqdm: '', xnxqSelectHtml: null };
  146. let source = '';
  147. if (pageHtml) {
  148. const fromPage = pickKbParams(new DOMParser().parseFromString(pageHtml, 'text/html'));
  149. if (fromPage.xhid && fromPage.xqdm) {
  150. params = fromPage;
  151. source = '课表页';
  152. }
  153. }
  154. if (!params.xhid || !params.xqdm) {
  155. const fromDom = pickKbParams(document);
  156. if (fromDom.xhid && fromDom.xqdm) {
  157. params = fromDom;
  158. source = '当前页面';
  159. }
  160. }
  161. if (!params.xhid || !params.xqdm) {
  162. throw new Error('无法获取课表页参数(#xhid / #xqdm),请确认已登录教务系统');
  163. }
  164. console.log(`JS: 参数来源=${source},xhid=${params.xhid.length} 字符,xqdm=${params.xqdm.length} 字符`);
  165. return params;
  166. }
  167. // 解析 #xnxq1 下拉:labels / values / 当前选中索引
  168. function parseSemesterOptions(selectHtml) {
  169. if (!selectHtml) return null;
  170. const doc = new DOMParser().parseFromString(selectHtml, 'text/html');
  171. const select = doc.querySelector('#xnxq1') || doc.querySelector('select[name="xnxq"]');
  172. if (!select) return null;
  173. const options = Array.from(select.querySelectorAll('option')).filter(o => (o.value || '').trim() !== '');
  174. if (options.length === 0) return null;
  175. const labels = options.map(o => (o.textContent || o.value || '').trim());
  176. const values = options.map(o => (o.value || '').trim());
  177. // 当前选中项(教务系统会把当前学期标为 selected)
  178. let selectedIndex = options.findIndex(o => o.selected);
  179. if (selectedIndex < 0) selectedIndex = -1;
  180. return { labels, values, selectedIndex: selectedIndex, defaultIndex: selectedIndex >= 0 ? selectedIndex : 0 };
  181. }
  182. // =========================================================================
  183. // 学期 / 校区选择
  184. // =========================================================================
  185. async function chooseSemester(selectHtml) {
  186. const options = parseSemesterOptions(selectHtml);
  187. if (!options) {
  188. throw new Error('未能读取学年学期下拉,请确认已登录教务系统');
  189. }
  190. // 默认静默采用教务系统标记的当前学期(可改为始终询问,见 ASK_SEMESTER)
  191. if (!ASK_SEMESTER && options.selectedIndex >= 0) {
  192. const picked = options.values[options.selectedIndex];
  193. console.log(`JS: 自动使用当前学期 ${picked}`);
  194. return picked;
  195. }
  196. // 弹出单选列表,并把“当前学期”作为默认选中项:
  197. // 常用场景直接确认即可;学期未开始时,也可在此改选下学期提前导入课表
  198. const index = await window.shiguangBridgePromise.showSingleSelection(
  199. "选择学年学期", JSON.stringify(options.labels), options.defaultIndex
  200. );
  201. if (index === null || index === -1) return null;
  202. return options.values[index];
  203. }
  204. async function chooseCampus(pageXqdm) {
  205. let campusList = null;
  206. try {
  207. const json = await getJson('/admin/api/jcsj/xqsj/getXqList');
  208. campusList = (json.data || [])
  209. .map(item => ({ xqdm: String(item.id || '').trim(), xqmc: String(item.xqmc || item.id || '').trim() }))
  210. .filter(item => item.xqdm && item.xqmc);
  211. } catch (error) {
  212. console.warn('JS: 获取校区列表失败:', error.message);
  213. }
  214. if (!campusList || campusList.length === 0) {
  215. toast("未获取到校区列表,使用当前页面校区继续导入。");
  216. return { xqdm: pageXqdm, xqmc: "" };
  217. }
  218. const matched = campusList.findIndex(item => item.xqdm === pageXqdm);
  219. if (!ASK_CAMPUS && matched >= 0) {
  220. console.log(`JS: 自动使用页面校区 ${campusList[matched].xqmc}`);
  221. return campusList[matched];
  222. }
  223. const index = await window.shiguangBridgePromise.showSingleSelection(
  224. "选择校区", JSON.stringify(campusList.map(item => item.xqmc)), matched >= 0 ? matched : 0
  225. );
  226. if (index === null || index === -1) return null;
  227. return campusList[index];
  228. }
  229. // =========================================================================
  230. // 作息时间与开学日期
  231. // =========================================================================
  232. // 返回 { timeSlots, semesterStartDate, semesterTotalWeeks }
  233. async function fetchTimeAndWeekData(xnxq, xqdm) {
  234. const json = await getJson(`/admin/api/getZclistByXnxq?xnxq=${encodeURIComponent(xnxq)}&xqid=${encodeURIComponent(xqdm)}`);
  235. const data = json.data || {};
  236. // 节次时间
  237. let timeSlots = null;
  238. if (Array.isArray(data.jcsjszList) && data.jcsjszList.length > 0) {
  239. timeSlots = data.jcsjszList
  240. .map(item => ({ number: Number(item.jc), startTime: formatTime(item.kssj), endTime: formatTime(item.jssj) }))
  241. .filter(item => item.number > 0 && item.startTime && item.endTime)
  242. .sort((a, b) => a.number - b.number);
  243. if (timeSlots.length === 0) timeSlots = null;
  244. }
  245. // 开学日期 = 第 1 周的周一
  246. let semesterStartDate = null;
  247. if (Array.isArray(data.zclist)) {
  248. const firstWeek = data.zclist.find(z => Number(z.zc) === 1);
  249. if (firstWeek && firstWeek.minrq) {
  250. semesterStartDate = String(firstWeek.minrq).split(' ')[0];
  251. }
  252. }
  253. const semesterTotalWeeks = Array.isArray(data.zclist) && data.zclist.length > 0
  254. ? data.zclist.length
  255. : FALLBACK_TOTAL_WEEKS;
  256. return { timeSlots, semesterStartDate, semesterTotalWeeks };
  257. }
  258. // =========================================================================
  259. // 课程数据
  260. // =========================================================================
  261. /**
  262. * 节次与周次合并去重函数
  263. * 采用 wiki 教程《课程合并与去重函数》的参考实现,未做修改
  264. * @param {Array<Object>} courses 原始解析课程数组
  265. * @returns {Array<Object>} 合并去重后的课程数组
  266. */
  267. function mergeAndDistinctCourses(courses) {
  268. if (!Array.isArray(courses) || courses.length <= 1) return courses;
  269. // 1. 深拷贝并规范周次数据,过滤无效项
  270. const list = courses.map(c => ({
  271. ...c,
  272. name: c.name || '',
  273. teacher: c.teacher || '',
  274. position: c.position || '',
  275. weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
  276. }));
  277. // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
  278. list.sort((a, b) => {
  279. return a.name.localeCompare(b.name) ||
  280. a.teacher.localeCompare(b.teacher) ||
  281. a.position.localeCompare(b.position) ||
  282. (a.day || 0) - (b.day || 0) ||
  283. a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
  284. (a.startSection || 0) - (b.startSection || 0);
  285. });
  286. const step1Merged = [];
  287. let current = list[0];
  288. for (let i = 1; i < list.length; i++) {
  289. const next = list[i];
  290. const isSameCourseAndWeeks =
  291. current.name === next.name &&
  292. current.teacher === next.teacher &&
  293. current.position === next.position &&
  294. current.day === next.day &&
  295. current.weeks.join(',') === next.weeks.join(',');
  296. const isContinuous = current.endSection + 1 === next.startSection;
  297. const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
  298. if (isSameCourseAndWeeks && isContinuous) {
  299. // 节次连续:延长结束节次 (如 1-2 节 + 3-4 节 -> 1-4 节)
  300. current.endSection = next.endSection;
  301. } else if (isSameCourseAndWeeks && isDuplicate) {
  302. // 完全重复:跳过
  303. continue;
  304. } else {
  305. step1Merged.push(current);
  306. current = next;
  307. }
  308. }
  309. step1Merged.push(current);
  310. // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
  311. step1Merged.sort((a, b) => {
  312. return a.name.localeCompare(b.name) ||
  313. a.teacher.localeCompare(b.teacher) ||
  314. a.position.localeCompare(b.position) ||
  315. (a.day || 0) - (b.day || 0) ||
  316. (a.startSection || 0) - (b.startSection || 0) ||
  317. (a.endSection || 0) - (b.endSection || 0);
  318. });
  319. const step2Merged = [];
  320. let cur = step1Merged[0];
  321. for (let i = 1; i < step1Merged.length; i++) {
  322. const nxt = step1Merged[i];
  323. const isSameCourseAndSection =
  324. cur.name === nxt.name &&
  325. cur.teacher === nxt.teacher &&
  326. cur.position === nxt.position &&
  327. cur.day === nxt.day &&
  328. cur.startSection === nxt.startSection &&
  329. cur.endSection === nxt.endSection;
  330. if (isSameCourseAndSection) {
  331. // 周次合并去重 (如 1-8 周 + 9-16 周 -> 1-16 周)
  332. cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
  333. } else {
  334. step2Merged.push(cur);
  335. cur = nxt;
  336. }
  337. }
  338. step2Merged.push(cur);
  339. return step2Merged;
  340. }
  341. function parseCourseData(jsonData) {
  342. if (!jsonData || !Array.isArray(jsonData.data)) return [];
  343. // 超星按“单节”返回(djc = 第几节):先逐节转成课程块(startSection = endSection = 节次),
  344. // 再交给 mergeAndDistinctCourses 处理连续节次、单双周与重复记录
  345. const rawCourses = [];
  346. for (const raw of jsonData.data) {
  347. const name = extractAnchorText(raw.kcmc);
  348. const teacher = cleanTeacherName(extractAnchorText(raw.tmc));
  349. const position = extractAnchorText(raw.croommc) || '待定';
  350. const day = Number(raw.xingqi);
  351. const section = Number(raw.djc);
  352. const weeks = parseWeeks(raw.zcstr);
  353. if (!name || isNaN(day) || isNaN(section) || day < 1 || day > 7 || section < 1 || weeks.length === 0) {
  354. continue;
  355. }
  356. rawCourses.push({
  357. name: name,
  358. teacher: teacher,
  359. position: position,
  360. day: day,
  361. startSection: section,
  362. endSection: section,
  363. weeks: weeks
  364. });
  365. }
  366. return mergeAndDistinctCourses(rawCourses);
  367. }
  368. async function fetchCourses(xnxq, xhid, xqdm) {
  369. const url = `/admin/xsd/pkgl/xskb/sdpkkbList?xnxq=${encodeURIComponent(xnxq)}&xhid=${encodeURIComponent(xhid)}&xqdm=${encodeURIComponent(xqdm)}&xskbxslx=0`;
  370. const json = await getJson(url);
  371. return parseCourseData(json);
  372. }
  373. // =========================================================================
  374. // 保存
  375. // =========================================================================
  376. async function saveCourses(courses) {
  377. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  378. }
  379. async function saveTimeSlots(timeSlots) {
  380. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  381. }
  382. async function saveConfig(semesterStartDate, semesterTotalWeeks) {
  383. const config = { semesterTotalWeeks: semesterTotalWeeks };
  384. if (semesterStartDate) config.semesterStartDate = semesterStartDate;
  385. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  386. }
  387. // =========================================================================
  388. // 主流程
  389. // =========================================================================
  390. function isLoginPage() {
  391. return window.location.href.includes('/admin/login') || window.location.href.includes('slogin');
  392. }
  393. async function runImportFlow() {
  394. try {
  395. if (isLoginPage()) {
  396. await alertUser("导入失败", "当前是登录页面,请先完成登录并进入教务系统内部页面后再执行导入。");
  397. return;
  398. }
  399. const confirmed = await alertUser(
  400. "湖北文理学院教务系统课表导入",
  401. "将自动读取当前学期、校区、作息时间与开学日期。\n导入前请确认已成功登录教务系统。"
  402. );
  403. if (confirmed !== true && confirmed !== "true") {
  404. toast("用户取消了导入。");
  405. return;
  406. }
  407. // 1. 参数(来自课表页而非当前页面,见 extractPageParams 注释)
  408. const { xhid, xqdm: pageXqdm, xnxqSelectHtml } = await extractPageParams();
  409. diagnoseInfo = `xhid ${xhid.length} 字符 / xqdm ${pageXqdm.length} 字符`;
  410. // 2. 学期(默认取教务系统标记的当前学期)
  411. const xnxq = await chooseSemester(xnxqSelectHtml);
  412. if (xnxq === null) {
  413. toast("导入已取消:未选择学年学期。");
  414. return;
  415. }
  416. // 3. 校区(默认取当前页面校区)
  417. const campus = await chooseCampus(pageXqdm);
  418. if (campus === null) {
  419. toast("导入已取消:未选择校区。");
  420. return;
  421. }
  422. toast(`正在导入 ${xnxq}${campus.xqmc ? ' · ' + campus.xqmc : ''} ...`);
  423. // 4. 课程
  424. const courses = await fetchCourses(xnxq, xhid, campus.xqdm);
  425. if (courses.length === 0) {
  426. await alertUser("未找到课程", `${xnxq} 学期未获取到任何课程数据,请确认学期选择是否正确,或该学期暂无排课。`);
  427. return;
  428. }
  429. await saveCourses(courses);
  430. console.log(`JS: 课程保存成功,共 ${courses.length} 条。`);
  431. // 5. 作息时间与开学日期(失败不阻塞课程导入)
  432. try {
  433. const { timeSlots, semesterStartDate, semesterTotalWeeks } = await fetchTimeAndWeekData(xnxq, campus.xqdm);
  434. await saveConfig(semesterStartDate, semesterTotalWeeks);
  435. if (timeSlots && timeSlots.length > 0) {
  436. await saveTimeSlots(timeSlots);
  437. } else {
  438. await saveTimeSlots(FALLBACK_TIME_SLOTS);
  439. toast("未获取到教务作息,已使用内置作息表。");
  440. }
  441. console.log(`JS: 配置导入完成,开学日期 ${semesterStartDate},总周数 ${semesterTotalWeeks}。`);
  442. } catch (error) {
  443. console.warn('JS: 作息/开学日期获取失败:', error.message);
  444. toast("作息时间与开学日期获取失败,课程已导入,可稍后重试。");
  445. }
  446. toast(`导入成功:${courses.length} 条课程、${xnxq}${campus.xqmc ? ' · ' + campus.xqmc : ''}。`);
  447. window.shiguangBridge.notifyTaskCompletion();
  448. } catch (error) {
  449. console.error('JS: 导入流程异常:', error);
  450. const tail = diagnoseInfo ? `\n\n[诊断] ${diagnoseInfo}` : '';
  451. await alertUser("导入失败", `${error.message}\n\n请确认:\n1. 已在教务系统中成功登录;\n2. 网络可以正常访问 hbuas.jw.chaoxing.com。${tail}`);
  452. }
  453. }
  454. runImportFlow();