xjtu.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. // 西安交通大学本科教务系统 (jwxt.xjtu.edu.cn / ehall.xjtu.edu.cn) 课表导入适配
  2. // -------- 配置常量 ---------
  3. // XJTU 作息时间(11节, 50分钟制)
  4. // 5月1日 - 10月1日: 夏季作息
  5. const SUMMER_TIME_SLOTS = [
  6. { number: 1, startTime: "08:00", endTime: "08:50" },
  7. { number: 2, startTime: "09:00", endTime: "09:50" },
  8. { number: 3, startTime: "10:10", endTime: "11:00" },
  9. { number: 4, startTime: "11:10", endTime: "12:00" },
  10. { number: 5, startTime: "14:30", endTime: "15:20" },
  11. { number: 6, startTime: "15:30", endTime: "16:20" },
  12. { number: 7, startTime: "16:40", endTime: "17:30" },
  13. { number: 8, startTime: "17:40", endTime: "18:30" },
  14. { number: 9, startTime: "19:40", endTime: "20:30" },
  15. { number: 10, startTime: "20:40", endTime: "21:30" },
  16. { number: 11, startTime: "21:40", endTime: "22:30" }
  17. ];
  18. // 10月1日 - 次年5月1日: 冬季作息
  19. const WINTER_TIME_SLOTS = [
  20. { number: 1, startTime: "08:00", endTime: "08:50" },
  21. { number: 2, startTime: "09:00", endTime: "09:50" },
  22. { number: 3, startTime: "10:10", endTime: "11:00" },
  23. { number: 4, startTime: "11:10", endTime: "12:00" },
  24. { number: 5, startTime: "14:00", endTime: "14:50" },
  25. { number: 6, startTime: "15:00", endTime: "15:50" },
  26. { number: 7, startTime: "16:10", endTime: "17:00" },
  27. { number: 8, startTime: "17:10", endTime: "18:00" },
  28. { number: 9, startTime: "19:10", endTime: "20:00" },
  29. { number: 10, startTime: "20:10", endTime: "21:00" },
  30. { number: 11, startTime: "21:10", endTime: "22:00" }
  31. ];
  32. // 周参数
  33. const FIRST_DAY_OF_WEEK = 1; // 每周第一天是 1
  34. const WEEK_DAYS = 7; // 一周有 7 天
  35. // 教学或总周数参数
  36. const DEFAULT_NUM_OF_WEEKS = 16;
  37. const MAX_NUM_OF_WEEKS = 32;
  38. // 课程参数
  39. const TOTAL_SECTION = SUMMER_TIME_SLOTS.length; // 根据作息表推断每天课程数
  40. const CLASS_DURATION = 50; // 每节课50分钟
  41. const BREAK_DURATION = 10; // 课间休息10分钟
  42. // 课程参数自检断言
  43. if (SUMMER_TIME_SLOTS.length !== WINTER_TIME_SLOTS.length) {
  44. throw new Error(":( 配置错误: 夏/冬作息表节数不一致, 请联系开发者"); // 开发用断言, 裸错误
  45. }
  46. // -------- 用户交互 --------
  47. /**
  48. * 生成学期选项列表 (用于兜底: 接口 xnxqcx 失败时使用)
  49. * 学期代码: 1=第一学期, 2=第二学期, 3=夏季小学期, 4=暑假
  50. * 范围: 当前学年前1年到后1年, 按新到旧排序, 与接口时间顺序一致
  51. * displayName 附带原始学期代码, 便于排错
  52. */
  53. function generateSemesterOptions(currentYear) {
  54. const semesterNames = { "1": "第一学期", "2": "第二学期", "3": "夏季小学期", "4": "暑假" };
  55. const semesterCodes = ["1", "2", "3", "4"];
  56. const options = [];
  57. const startYear = parseInt(currentYear.split("-")[0], 10) - 1;
  58. const endYear = parseInt(currentYear.split("-")[0], 10) + 1;
  59. for (let y = endYear; y >= startYear; y--) {
  60. const yearCode = `${y}-${y + 1}`;
  61. for (const semCode of semesterCodes) {
  62. const xnxqdm = `${yearCode}-${semCode}`;
  63. const displayName = `${yearCode}学年 ${semesterNames[semCode] || semCode} (${xnxqdm})`;
  64. options.push({ xnxqdm, displayName });
  65. }
  66. }
  67. return options;
  68. }
  69. /**
  70. * 从教务系统动态获取学期列表 (裁剪到当前学年起前3个学年后1个学年, 共5学年)
  71. * 拿到后显式按 DM 倒序 (时间倒序, 同学年内: 暑假->小学期->第二学期->第一学期)
  72. * 返回 [{xnxqdm, displayName}], 失败时回退到手写列表
  73. */
  74. async function fetchTermOptions(currentTerm) {
  75. const currentYear = `${currentTerm.split("-")[0]}-${currentTerm.split("-")[1]}`;
  76. const yearStart = parseInt(currentTerm.split("-")[0], 10);
  77. let rows = null;
  78. try {
  79. const res = await api("/jwapp/sys/wdkb/modules/jshkcb/xnxqcx.do", { data: "*order=-DM" });
  80. rows = res?.datas?.xnxqcx?.rows;
  81. } catch (e) {
  82. console.warn("获取学期列表失败, 使用兜底列表:", e.message);
  83. }
  84. if (Array.isArray(rows) && rows.length > 0) {
  85. // 显式按 DM 字符串倒序 (YYYY-YYYY-N 格式下等价于时间倒序), 不依赖接口
  86. rows.sort((a, b) => (b?.DM || "").localeCompare(a?.DM || ""));
  87. const options = [];
  88. for (const row of rows) {
  89. const dm = row?.DM ? String(row.DM) : "";
  90. if (!dm) continue;
  91. const y = parseInt(dm.split("-")[0], 10);
  92. // 裁剪: 保留当前学年起前3个学年后1个学年 (共5学年)
  93. if (!Number.isInteger(y) || y < yearStart - 3 || y > yearStart + 1) continue;
  94. const mc = row?.MC ? String(row.MC).trim() : "";
  95. options.push({ xnxqdm: dm, displayName: `${mc || dm} (${dm})` });
  96. }
  97. if (options.length > 0) {
  98. console.log(`获取到 ${options.length} 个学期选项 (5学年范围内)`);
  99. return options;
  100. }
  101. } else {
  102. console.warn("学期列表为空, 使用兜底列表");
  103. }
  104. return generateSemesterOptions(currentYear);
  105. }
  106. /**
  107. * 让用户选择学年学期
  108. * 学期列表从教务系统动态获取 (失败回退手写列表)
  109. * 返回所选学期代码, 用户取消时返回 null
  110. */
  111. async function selectTerm(currentTerm) {
  112. const options = await fetchTermOptions(currentTerm);
  113. const displayNames = options.map(o => o.displayName);
  114. // 默认选中当前学期, 找不到时 clamp 到 0
  115. let defaultIndex = options.findIndex(o => o.xnxqdm === currentTerm);
  116. if (defaultIndex < 0) defaultIndex = 0;
  117. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  118. "选择学期",
  119. JSON.stringify(displayNames),
  120. defaultIndex
  121. );
  122. if (selectedIndex === null || selectedIndex < 0 || selectedIndex >= options.length) {
  123. window.shiguangBridge.showToast("'_? 用户取消了导入");
  124. return null;
  125. }
  126. return options[selectedIndex].xnxqdm;
  127. }
  128. /**
  129. * 显示导入确认提示
  130. */
  131. async function promptUserToStart() {
  132. const confirmed = await window.shiguangBridgePromise.showAlert(
  133. "导入确认",
  134. "即将从教务系统导入课表, 请确保您已登录教务系统\n导入完成后请在App中仔细核对每门课程的时间, 地点和周次是否与教务系统一致, 勿漏为要",
  135. "我会确认, 开始吧"
  136. );
  137. if (!confirmed) {
  138. window.shiguangBridge.showToast("'_? 用户取消了导入");
  139. return false;
  140. }
  141. return true;
  142. }
  143. // -------- 请求工具 --------
  144. async function api(url, options = {}) {
  145. const headers = {
  146. "accept": "application/json, text/javascript, */*; q=0.01",
  147. "x-requested-with": "XMLHttpRequest",
  148. ...(options.data && { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" }),
  149. ...options.headers
  150. };
  151. let res;
  152. try {
  153. res = await fetch(url, {
  154. method: options.data ? "POST" : "GET",
  155. headers: headers,
  156. body: options.data || null,
  157. credentials: "include"
  158. });
  159. } catch (e) {
  160. throw new Error(`:( 网络请求失败 (${url}), 请检查网络连接`);
  161. }
  162. if (!res.ok) {
  163. throw new Error(`:( 请求失败 (HTTP ${res.status}), 请确认已登录教务系统`);
  164. }
  165. try {
  166. return await res.json();
  167. } catch (e) {
  168. console.error(":( 响应内容无法解析为 JSON:", url, e);
  169. throw new Error(":( 教务系统返回了无法解析的数据, 请确认已登录教务系统");
  170. }
  171. }
  172. // -------- 1. 获取学期信息 --------
  173. async function getTermInfo() {
  174. // 1. 当前学期代码, 如 "2025-2026-1" (作为默认选项 + 兜底校验)
  175. const termRes = await api("/jwapp/sys/wdkb/modules/jshkcb/dqxnxq.do");
  176. const rows = termRes?.datas?.dqxnxq?.rows || [];
  177. const currentTerm = rows[0]?.DM;
  178. if (!currentTerm) {
  179. throw new Error(":( 无法获取当前学期代码, 请确认已登录教务系统");
  180. }
  181. console.log("检测到当前学期:", currentTerm);
  182. // 2. 让用户选择学期 (默认当前学期), 取消则整体终止
  183. const term = await selectTerm(currentTerm);
  184. if (!term) return null;
  185. console.log(">>> 已选择学期:", term);
  186. // 3. 所选学期的开学日期与周数 (失败提示, 非致命错误)
  187. let semesterStartDate = null;
  188. let termWeeks = null;
  189. try {
  190. // term 格式为 "学年1-学年2-学期序号", 如 "2026-2027-1"
  191. const [xn, xn2, xq] = term.split("-");
  192. const params = `XN=${xn}-${xn2}&XQ=${xq}`;
  193. console.log("查询学期开始日期参数:", params);
  194. const startRes = await api("/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do", {
  195. data: params
  196. });
  197. const row = startRes?.datas?.cxjcs?.rows?.[0];
  198. // 严格解析 YYYY-MM-DD, 兼容可能的 "2026-09-14 00:00:00" 及 ISO 格式
  199. // 不匹配置 null 走非致命路径
  200. const dateMatch = row?.XQKSRQ ? String(row.XQKSRQ).match(/^\d{4}-\d{2}-\d{2}/) : null;
  201. if (dateMatch) {
  202. semesterStartDate = dateMatch[0];
  203. }
  204. // ZJXZC: 教学周数(首选); ZZC: 学期总周数(含考试周, 次选)
  205. const parseWeeksField = (v) => {
  206. const s = String(v ?? "");
  207. if (!/^\d+$/.test(s)) return null;
  208. const n = parseInt(s, 10);
  209. return (n >= 1 && n <= MAX_NUM_OF_WEEKS) ? n : null;
  210. };
  211. termWeeks = parseWeeksField(row?.ZJXZC) || parseWeeksField(row?.ZZC) || null;
  212. } catch (e) {
  213. console.warn("获取学期开始日期失败(不影响导入):", e.message);
  214. }
  215. if (!semesterStartDate) {
  216. console.warn("未获取到学期开始日期(警告将在导入结果中提示)");
  217. }
  218. console.log("学期信息:", { semesterStartDate, termWeeks });
  219. return { term, semesterStartDate, termWeeks };
  220. }
  221. // -------- 2. 获取课表原始数据 --------
  222. async function getCourseRows(term) {
  223. const res = await api("/jwapp/sys/wdkb/modules/xskcb/xskcb.do", {
  224. data: `XNXQDM=${encodeURIComponent(term)}`
  225. });
  226. const rows = res?.datas?.xskcb?.rows;
  227. console.log(`获取到 ${Array.isArray(rows) ? rows.length : 0} 条课表原始数据`);
  228. if (!Array.isArray(rows) || rows.length === 0) {
  229. // 已保证 `rows.length > 0`
  230. throw new Error(":( 该学期暂无课程数据(可能尚未排课)");
  231. }
  232. return rows;
  233. }
  234. // -------- 3. 解析与合并 --------
  235. /**
  236. * 解析周次位图字符串, 如 "1111111111111111000" -> [1,2,...,16]
  237. * 格式校验: 仅允许 0/1 字符, 非法格式panic
  238. */
  239. function parseWeekBitmap(name, bitmap) {
  240. if (!bitmap || typeof bitmap !== "string" || !/^[01]+$/.test(bitmap)) {
  241. throw new Error(`:( 课程 ${name} 数据异常: 周次格式非法 (${bitmap})`);
  242. }
  243. const weeks = [];
  244. for (let i = 0; i < bitmap.length; i++) {
  245. if (bitmap[i] === "1") {
  246. weeks.push(i + 1);
  247. }
  248. }
  249. return weeks;
  250. }
  251. /**
  252. * 检查上课日期(星期几), 开始/结束节次是否为整数, 若教务修改接口返回非纯`Int`字段, 应当panic
  253. * 格式校验: 仅允许数字传入
  254. */
  255. function parseStrictUnsizedInt(name, field, value) {
  256. const s = String(value ?? "");
  257. if (!/^\d+$/.test(s)) {
  258. throw new Error(`:( 课程 ${name} 数据异常: ${field}非法 (${value})`);
  259. }
  260. const result = parseInt(s, 10);
  261. if (result < 1) {
  262. throw new Error(`:( 课程 ${name} 数据异常: ${field} 中 ${value} 不能小于 1`);
  263. }
  264. return result;
  265. }
  266. /**
  267. * 统一教师字段格式: 拆分, 去空白, 排序后重组
  268. * 避免 "张三,李四" 与 "李四,张三" 因顺序不同而漏合并
  269. */
  270. function normalizeTeacher(teacher) {
  271. if (!teacher) return "未知教师";
  272. return teacher
  273. .split(/[,,、]/)
  274. .map(name => name.trim())
  275. .filter(name => name.length > 0)
  276. .sort()
  277. .join(",");
  278. }
  279. /**
  280. * 将单条原始数据映射为课程片段
  281. * 校验: 课程名, 星期(1-7), 周次任一非法即panic
  282. * 节次处理: 1-11 正常, 出现第12节即非法, panic
  283. */
  284. function parseCourseRow(row) {
  285. if (!row || typeof row !== "object") {
  286. throw new Error(":( 课表数据异常: 课程条目格式非法");
  287. }
  288. const name = row.KCM ? String(row.KCM).trim() : "";
  289. if (!name) {
  290. throw new Error(":( 课表数据异常: 某条课程缺少课程名");
  291. }
  292. const day = parseStrictUnsizedInt(name, "SKXQ", row.SKXQ);
  293. // `parseStrictUnsizedInt`解析, 确保 day >= 1
  294. if (day > WEEK_DAYS) {
  295. throw new Error(`:( 课程 ${name} 数据异常: 星期值非法 (${row.SKXQ})`);
  296. }
  297. const startSection = parseStrictUnsizedInt(name, "KSJC", row.KSJC);
  298. const endSection = parseStrictUnsizedInt(name, "JSJC", row.JSJC);
  299. // 出现第12节或开始节次大于结束节次: 教务格式不可信, panic
  300. if (startSection > endSection || endSection > TOTAL_SECTION) {
  301. throw new Error(`:( 课程 ${name} 数据异常: 节次非法 (${row.KSJC}-${row.JSJC})`);
  302. }
  303. const weeks = parseWeekBitmap(name, String(row.SKZC ?? ""));
  304. // 全零位图(如 "0000...")能通过格式校验但解析出空周次, 非法数据, panic
  305. if (weeks.length === 0) {
  306. throw new Error(`:( 课程 ${name} 数据异常: 周次信息缺失`);
  307. }
  308. const teacher = normalizeTeacher(row.SKJS);
  309. // 上课地点为空时用校区名
  310. const position = (row.JASMC && String(row.JASMC).trim() !== "")
  311. ? String(row.JASMC).trim()
  312. : (row.XXXQDM_DISPLAY || "未知地点");
  313. return {
  314. course: {
  315. name: name,
  316. teacher: teacher,
  317. position: position,
  318. day: day,
  319. startSection: startSection,
  320. endSection: endSection,
  321. weeks: weeks
  322. }
  323. };
  324. }
  325. /**
  326. * 合并去重: 课程名, 教师, 地点, 星期, 节次相同(仅周次不同)的片段合并为一条,
  327. * 周次取并集, 完全重复的行也在此步骤合并
  328. */
  329. function mergeCourses(courseList) {
  330. const merged = new Map();
  331. for (const course of courseList) {
  332. const key = [
  333. course.name,
  334. course.teacher,
  335. course.position,
  336. course.day,
  337. course.startSection,
  338. course.endSection
  339. ].join("|");
  340. if (merged.has(key)) {
  341. const existing = merged.get(key);
  342. const union = [...new Set([...existing.weeks, ...course.weeks])].sort((a, b) => a - b);
  343. existing.weeks = union;
  344. } else {
  345. merged.set(key, { ...course, weeks: [...course.weeks] });
  346. }
  347. }
  348. const result = [...merged.values()];
  349. console.log(`合并去重: 原始 ${courseList.length} 条 -> 合并后 ${result.length} 条`);
  350. return result;
  351. }
  352. // -------- 4. 汇总解析 --------
  353. function parseAllCourses(rows, termWeeks) {
  354. const courseList = [];
  355. for (const row of rows) {
  356. courseList.push(parseCourseRow(row).course);
  357. }
  358. const courses = mergeCourses(courseList);
  359. // 学期总周数: 取接口教学周数与课程最大周的较大者, 默认 16 兜底
  360. let maxCourseWeek = 0;
  361. for (const course of courses) {
  362. for (const week of course.weeks) {
  363. if (week > maxCourseWeek) maxCourseWeek = week;
  364. }
  365. }
  366. const totalWeeks = Math.max(termWeeks || 0, maxCourseWeek) || DEFAULT_NUM_OF_WEEKS;
  367. console.log(`学期总周数: ${totalWeeks} (接口值: ${termWeeks}, 课程最大周: ${maxCourseWeek})`);
  368. // 作息时间: 按当前日期选择(5月-9月为夏季作息)
  369. const month = new Date().getMonth() + 1;
  370. const isSummer = month >= 5 && month <= 9;
  371. const timeSlots = isSummer ? SUMMER_TIME_SLOTS : WINTER_TIME_SLOTS;
  372. console.log(`当前为${isSummer ? "夏季" : "冬季"}作息`);
  373. return {
  374. courses,
  375. timeSlots,
  376. totalWeeks,
  377. seasonTip: isSummer ? "已按当前日期使用夏季作息 (5/1-10/1)" : "已按当前日期使用冬季作息 (10/1-次年5/1)"
  378. };
  379. }
  380. // -------- 5. 保存流程 --------
  381. // 仅在拿到开学日期时调用
  382. // 原因: saveCourseConfig 是整体覆盖语义, bridge 会把缺失字段补成默认值写入,
  383. // 不带开学日期调用会清空用户原有配置, 而开学日期是周次推算的起点, 丢失可能导致周次全错
  384. async function saveConfig(semesterStartDate, totalWeeks) {
  385. const configData = {
  386. semesterStartDate: semesterStartDate,
  387. semesterTotalWeeks: totalWeeks || DEFAULT_NUM_OF_WEEKS,
  388. defaultClassDuration: CLASS_DURATION,
  389. defaultBreakDuration: BREAK_DURATION,
  390. firstDayOfWeek: FIRST_DAY_OF_WEEK
  391. };
  392. const success = await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(configData));
  393. if (!success) {
  394. throw new Error(":( 学期配置保存失败");
  395. }
  396. console.log("课表配置保存成功:", configData);
  397. }
  398. async function saveTimeSlots(timeSlots) {
  399. const success = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  400. if (!success) {
  401. throw new Error(":( 时间段保存失败");
  402. }
  403. console.log("时间段保存成功:", timeSlots);
  404. }
  405. async function saveCourses(courses) {
  406. const success = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  407. if (!success) {
  408. throw new Error(":( 课程数据保存失败");
  409. }
  410. console.log(`课程数据保存成功, 共 ${courses.length} 条`);
  411. }
  412. // -------- 主导入流程 --------
  413. /**
  414. * 单次导入尝试
  415. * 返回值:
  416. * "success" - 导入成功 (已通知App)
  417. * "cancel" - 用户主动取消或无需重试的流程终止 (静默结束)
  418. * "retry" - 导入失败且用户选择重试
  419. */
  420. async function importOnce() {
  421. try {
  422. const termInfo = await getTermInfo();
  423. // 用户在学期选择弹窗取消: 静默退出, 不走 catch 的失败提示
  424. if (!termInfo) return "cancel";
  425. const rows = await getCourseRows(termInfo.term);
  426. const { courses, timeSlots, totalWeeks, seasonTip } = parseAllCourses(rows, termInfo.termWeeks);
  427. // saveCourseConfig 为整体覆盖语义: 无开学日期时跳过写入, 避免清空用户已有配置
  428. // (开学日期是周次推算的起点, 不应错误, 由用户在App中手动设置)
  429. // 警告并入最终成功提示, 避免被后续 toast 顶掉
  430. let configSkipTip = "";
  431. if (termInfo.semesterStartDate) {
  432. await saveConfig(termInfo.semesterStartDate, totalWeeks);
  433. } else {
  434. console.warn("跳过学期配置写入(无开学日期)");
  435. const semCode = termInfo.term.split("-")[2];
  436. const isUnconventional = semCode === "3" || semCode === "4";
  437. configSkipTip = isUnconventional
  438. ? ". 注意: 未获取到开学日期, 未写入学期配置, 请在App中手动设置开学日期并仔细核对周次"
  439. : ". 注意: 未获取到开学日期, 未写入学期配置, 请在App中手动设置开学日期并核对周次";
  440. }
  441. await saveTimeSlots(timeSlots);
  442. await saveCourses(courses);
  443. window.shiguangBridge.showToast(`>_ 导入成功! 共 ${courses.length} 条课程 (${seasonTip})${configSkipTip}`);
  444. window.shiguangBridge.notifyTaskCompletion();
  445. return "success";
  446. } catch (error) {
  447. console.error(":( 主流程异常:", error);
  448. // 失败后提供重试入口, 无需刷新网页 (脚本在本页持续等待用户操作)
  449. // 弹窗关闭/取消即视为放弃重试, 天然不会死循环
  450. try {
  451. const wantsRetry = await window.shiguangBridgePromise.showAlert(
  452. "导入失败",
  453. `${error.message}\n\n可点击重试, 无需刷新网页`,
  454. "重试导入"
  455. );
  456. return wantsRetry ? "retry" : "cancel";
  457. } catch (alertError) {
  458. // 重试弹窗自身异常(如bridge故障): 降级为放弃重试, 避免未处理的Promise rejection
  459. console.error(":( 重试弹窗显示失败:", alertError);
  460. return "cancel";
  461. }
  462. }
  463. }
  464. async function runImportFlow() {
  465. let isReady;
  466. try {
  467. isReady = await promptUserToStart();
  468. } catch (err) {
  469. console.error(":( 确认弹窗异常:", err);
  470. return; // bridge 不通, 放弃
  471. }
  472. // 开始确认仅询问一次, 重试时跳过
  473. if (!isReady) return;
  474. while (true) {
  475. const result = await importOnce();
  476. if (result !== "retry") break;
  477. window.shiguangBridge.showToast(">>> 重试导入");
  478. }
  479. }
  480. // 启动导入流程
  481. runImportFlow();