فهرست منبع

Merge pull request #306 from XingHeYuZhuan/pending

星河欲转 1 ماه پیش
والد
کامیت
39cf384d89

+ 16 - 1
index/root_index.yaml

@@ -480,4 +480,19 @@ schools:
   - id: "STCNCHU"
     name: "南昌航空大学科技学院"
     initial: "N"
-    resource_folder: "STCNCHU"
+    resource_folder: "STCNCHU"
+
+  - id: "CFEC"
+    name: "重庆财经学院"
+    initial: "C"
+    resource_folder: "CFEC"
+
+  - id: "AHZYYGZ"
+    name: "安徽中医药高等专科学校"
+    initial: "A"
+    resource_folder: "AHZYYGZ"
+
+  - id: "XATU"
+    name: "西安工业大学"
+    initial: "X"
+    resource_folder: "XATU"

+ 423 - 0
resources/AHZYYGZ/AHZYYGZ.js

@@ -0,0 +1,423 @@
+const BASE = `${window.location.origin}/ahzyygzjw`;
+const CONTROL_PAGE = '/student/xkjg.wdkb.jsp?menucode=S20301';
+const TIMETABLE_PAGE = '/student/wsxk.xskcb10319.jsp?params=';
+const TIME_SLOTS = [
+  { number: 1, startTime: '08:00', endTime: '08:40' },
+  { number: 2, startTime: '08:50', endTime: '09:30' },
+  { number: 3, startTime: '09:45', endTime: '10:25' },
+  { number: 4, startTime: '10:35', endTime: '11:15' },
+  { number: 5, startTime: '11:25', endTime: '12:05' },
+  { number: 6, startTime: '14:00', endTime: '14:40' },
+  { number: 7, startTime: '14:50', endTime: '15:30' },
+  { number: 8, startTime: '15:45', endTime: '16:25' },
+  { number: 9, startTime: '16:35', endTime: '17:15' },
+  { number: 10, startTime: '19:00', endTime: '20:00' },
+  { number: 11, startTime: '20:00', endTime: '21:00' },
+  { number: 12, startTime: '21:40', endTime: '22:30' }
+];
+
+function cleanText(value) {
+  return String(value || '')
+    .replace(/[​-‍]/g, '')
+    .replace(/ /g, ' ')
+    .trim();
+}
+
+function encodeParams(xn, xq, xh) {
+  return btoa(`xn=${xn}&xq=${xq}&xh=${xh}`);
+}
+
+function parseWeeks(weekStr) {
+  const weeks = [];
+  String(weekStr || '')
+    .replace(/\s+/g, '')
+    .split(/[,,]/)
+    .forEach((part) => {
+      if (!part) return;
+      const isSingle = part.includes('单');
+      const isDouble = part.includes('双');
+      const rangeMatch = part.match(/(\d+)-(\d+)/);
+      if (rangeMatch) {
+        const start = parseInt(rangeMatch[1], 10);
+        const end = parseInt(rangeMatch[2], 10);
+        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(part.replace(/[^\d]/g, ''), 10);
+        if (!Number.isNaN(num)) weeks.push(num);
+      }
+    });
+  return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+function decodeParams(encoded) {
+  try {
+    return atob(encoded);
+  } catch (_) {
+    return '';
+  }
+}
+
+function parseXhFromEncodedParams(encoded) {
+  const decoded = decodeParams(encoded);
+  if (!decoded) return '';
+  const search = new URLSearchParams(decoded);
+  return String(search.get('xh') || '').trim();
+}
+
+function extractParamsFromHtml(html) {
+  const match = String(html || '').match(/wsxk\.xskcb10319\.jsp\?params=([^"'&\s>]+)/);
+  return match ? decodeURIComponent(match[1]) : '';
+}
+
+function findControlFrame(win) {
+  try {
+    if (win.document.querySelector('#xnxq')) return win;
+  } catch (_) {}
+  for (let i = 0; i < win.frames.length; i++) {
+    try {
+      const found = findControlFrame(win.frames[i]);
+      if (found) return found;
+    } catch (_) {}
+  }
+  return null;
+}
+
+function findTimetableFrame(win) {
+  try {
+    if (win.document.getElementById('mytable')) return win;
+  } catch (_) {}
+  for (let i = 0; i < win.frames.length; i++) {
+    try {
+      const found = findTimetableFrame(win.frames[i]);
+      if (found) return found;
+    } catch (_) {}
+  }
+  return null;
+}
+
+async function fetchControlDoc() {
+  const res = await fetch(`${BASE}${CONTROL_PAGE}`, { credentials: 'include' });
+  if (!res.ok) throw new Error(`课表控制页请求失败: ${res.status}`);
+  const html = await res.text();
+  return new DOMParser().parseFromString(html, 'text/html');
+}
+
+async function fetchTimetableDoc(xn, xq, xh) {
+  const params = encodeParams(xn, xq, xh);
+  const res = await fetch(`${BASE}${TIMETABLE_PAGE}${encodeURIComponent(params)}`, {
+    method: 'GET',
+    credentials: 'include'
+  });
+  if (!res.ok) throw new Error(`课表页面请求失败: ${res.status}`);
+  const buffer = await res.arrayBuffer();
+  let html = '';
+  try {
+    html = new TextDecoder('gbk').decode(buffer);
+  } catch (_) {
+    html = new TextDecoder('utf-8').decode(buffer);
+  }
+  return new DOMParser().parseFromString(html, 'text/html');
+}
+
+function parseSelectOptions(selectEl) {
+  if (!selectEl) return { options: [], defaultIndex: 0 };
+  const options = [];
+  let defaultIndex = 0;
+  Array.from(selectEl.querySelectorAll('option')).forEach((opt) => {
+    const value = String(opt.value || '').trim();
+    if (!value) return;
+    const text = cleanText(opt.textContent) || value;
+    if (opt.selected) defaultIndex = options.length;
+    options.push({ value, text });
+  });
+  return { options, defaultIndex };
+}
+
+async function resolveTermSelection() {
+  let controlDoc = null;
+  let controlFrame = findControlFrame(window);
+
+  if (controlFrame) {
+    controlDoc = controlFrame.document;
+  } else {
+    controlDoc = await fetchControlDoc();
+  }
+
+  const select = controlDoc.querySelector('#xnxq');
+  if (!select) {
+    throw new Error('未找到学期选择器,请先登录并打开“学生个人课表”页面');
+  }
+
+  const { options, defaultIndex } = parseSelectOptions(select);
+  if (!options.length) {
+    throw new Error('未读取到学期列表,请先进入“学生个人课表”页面');
+  }
+
+  const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
+    '选择学期',
+    JSON.stringify(options.map(item => item.text)),
+    defaultIndex
+  );
+  if (selectedIndex === null || selectedIndex === -1) {
+    throw new Error('已取消学期选择');
+  }
+
+  const selected = options[selectedIndex];
+  const [xn, xq] = String(selected.value).split('-');
+  if (!xn || typeof xq === 'undefined') {
+    throw new Error(`学期值解析失败: ${selected.value}`);
+  }
+
+  let encodedParams = extractParamsFromHtml(controlDoc.documentElement.outerHTML);
+  if (!encodedParams) {
+    const timetableFrame = findTimetableFrame(window);
+    if (timetableFrame) {
+      const url = new URL(timetableFrame.location.href);
+      encodedParams = url.searchParams.get('params') || '';
+    }
+  }
+
+  const xh = parseXhFromEncodedParams(encodedParams);
+  if (!xh) {
+    throw new Error('未读取到学号参数,请先点击进入“学生个人课表”后再导入');
+  }
+
+  return { xn, xq, xh, selectedValue: selected.value };
+}
+
+function findTable(doc) {
+  return doc.getElementById('mytable')
+    || Array.from(doc.querySelectorAll('table')).find(table => {
+      const text = cleanText(table.innerText);
+      return text.includes('星期一') && text.includes('[');
+    })
+    || null;
+}
+
+function parseCourseFromLines(lines, day) {
+  const joined = lines.join('\n');
+  const match = joined.match(/([\d,-单双]+)\[(\d+)-(\d+)\]/);
+  if (!match) return null;
+
+  const before = joined
+    .slice(0, match.index)
+    .split(/\n+/)
+    .map(cleanText)
+    .filter(Boolean);
+  const after = joined
+    .slice(match.index + match[0].length)
+    .split(/\n+/)
+    .map(cleanText)
+    .filter(Boolean);
+
+  let name = '';
+  let teacher = '';
+  if (before.length >= 2) {
+    name = before[0];
+    teacher = before[1];
+  } else if (before.length === 1) {
+    name = before[0];
+  }
+  if (!name) return null;
+
+  return {
+    name,
+    teacher,
+    position: after.join(' '),
+    day,
+    startSection: parseInt(match[2], 10),
+    endSection: parseInt(match[3], 10),
+    weeks: parseWeeks(match[1])
+  };
+}
+
+function parseCellByDivBlocks(cell, day) {
+  const blocks = Array.from(cell.querySelectorAll('div[style*="padding-bottom:5px"], div[style*="padding-bottom: 5px"]'));
+  if (!blocks.length) return [];
+
+  const items = [];
+  blocks.forEach((block) => {
+    const lines = block.innerText
+      .split(/\n+/)
+      .map(cleanText)
+      .filter(Boolean);
+    const parsed = parseCourseFromLines(lines, day);
+    if (parsed && parsed.weeks.length) items.push(parsed);
+  });
+  return items;
+}
+
+function parseCellByTextFallback(cell, day) {
+  const lines = cell.innerText
+    .split(/\n+/)
+    .map(cleanText)
+    .filter(Boolean);
+  if (!lines.length) return [];
+
+  const timeIndices = [];
+  lines.forEach((line, index) => {
+    if (/([\d,-单双]+)\[(\d+)-(\d+)\]/.test(line)) {
+      timeIndices.push(index);
+    }
+  });
+
+  const items = [];
+  timeIndices.forEach((currentIndex, i) => {
+    const nextIndex = i + 1 < timeIndices.length ? timeIndices[i + 1] : lines.length;
+    let beforeLines = i === 0 ? lines.slice(0, currentIndex) : lines.slice(timeIndices[i - 1] + 1, currentIndex);
+    if (beforeLines.length > 2) beforeLines = beforeLines.slice(beforeLines.length - 2);
+
+    const match = lines[currentIndex].match(/([\d,-单双]+)\[(\d+)-(\d+)\]/);
+    if (!match) return;
+
+    const name = beforeLines[0] || '未知课程';
+    const teacher = beforeLines[1] || '';
+    const positionLines = lines.slice(currentIndex + 1, nextIndex);
+
+    items.push({
+      name,
+      teacher,
+      position: positionLines.join(' '),
+      day,
+      startSection: parseInt(match[2], 10),
+      endSection: parseInt(match[3], 10),
+      weeks: parseWeeks(match[1])
+    });
+  });
+
+  return items;
+}
+
+function parseAndMergeQingguoTable(doc) {
+  const table = findTable(doc);
+  if (!table) {
+    throw new Error('未找到课表表格,请先进入“学生个人课表”页面');
+  }
+
+  const rawItems = [];
+  Array.from(table.rows).forEach((row) => {
+    const cells = Array.from(row.cells);
+    if (cells.length < 7) return;
+
+    cells.forEach((cell, colIndex) => {
+      const distanceToLast = cells.length - 1 - colIndex;
+      if (distanceToLast > 6) return;
+      const day = 7 - distanceToLast;
+      const rawText = cleanText(cell.innerText);
+      if (!rawText.includes('[')) return;
+
+      const divParsed = parseCellByDivBlocks(cell, day);
+      if (divParsed.length) {
+        rawItems.push(...divParsed);
+        return;
+      }
+
+      rawItems.push(...parseCellByTextFallback(cell, day));
+    });
+  });
+
+  const groupMap = new Map();
+  rawItems.forEach((item) => {
+    if (!item || !item.name || !item.weeks.length) return;
+    const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
+    if (!groupMap.has(key)) groupMap.set(key, {});
+    const weekMap = groupMap.get(key);
+    item.weeks.forEach((week) => {
+      if (!weekMap[week]) weekMap[week] = new Set();
+      for (let section = item.startSection; section <= item.endSection; section++) {
+        weekMap[week].add(section);
+      }
+    });
+  });
+
+  const finalCourses = [];
+  groupMap.forEach((weekMap, key) => {
+    const [name, teacher, position, day] = key.split('|');
+    const patternMap = new Map();
+
+    Object.keys(weekMap).forEach((weekStr) => {
+      const week = parseInt(weekStr, 10);
+      const sections = Array.from(weekMap[week]).sort((a, b) => a - b);
+      if (!sections.length) return;
+      let start = sections[0];
+      for (let i = 0; i < sections.length; i++) {
+        if (i === sections.length - 1 || sections[i + 1] !== sections[i] + 1) {
+          const pKey = `${start}-${sections[i]}`;
+          if (!patternMap.has(pKey)) patternMap.set(pKey, []);
+          patternMap.get(pKey).push(week);
+          if (i < sections.length - 1) start = sections[i + 1];
+        }
+      }
+    });
+
+    patternMap.forEach((weeks, patternKey) => {
+      const [startSection, endSection] = patternKey.split('-').map(Number);
+      finalCourses.push({
+        name,
+        teacher,
+        position,
+        day: parseInt(day, 10),
+        startSection,
+        endSection,
+        weeks: weeks.sort((a, b) => a - b)
+      });
+    });
+  });
+
+  return finalCourses;
+}
+
+async function loadTimetableDoc(term) {
+  const currentTableFrame = findTimetableFrame(window);
+  if (currentTableFrame && currentTableFrame.document.getElementById('mytable')) {
+    const currentUrl = new URL(currentTableFrame.location.href);
+    const currentParams = currentUrl.searchParams.get('params') || '';
+    const decoded = decodeParams(currentParams);
+    if (decoded.includes(`xn=${term.xn}`) && decoded.includes(`xq=${term.xq}`)) {
+      return currentTableFrame.document;
+    }
+  }
+  return await fetchTimetableDoc(term.xn, term.xq, term.xh);
+}
+
+async function runImportFlow() {
+  try {
+    const confirmed = await window.AndroidBridgePromise.showAlert(
+      '安徽中医药高等专科学校教务导入',
+      '请确认你已登录教务系统,并且最好已经打开“学生个人课表”页面。',
+      '确定,开始导入'
+    );
+    if (!confirmed) return;
+
+    const term = await resolveTermSelection();
+    AndroidBridge.showToast('正在提取青果课表数据...');
+    const doc = await loadTimetableDoc(term);
+    const courses = parseAndMergeQingguoTable(doc);
+    if (!courses.length) {
+      throw new Error('未找到有效课程,请确认当前学期课表已正常显示');
+    }
+
+    const allWeeks = courses.flatMap(course => course.weeks);
+    const semesterTotalWeeks = allWeeks.length ? Math.max(...allWeeks) : 20;
+
+    await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({
+      semesterTotalWeeks,
+      semesterStartDate: null,
+      firstDayOfWeek: 1
+    }));
+    await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(TIME_SLOTS));
+    await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+
+    AndroidBridge.showToast(`导入成功:共 ${courses.length} 门课程`);
+    AndroidBridge.notifyTaskCompletion();
+  } catch (error) {
+    console.error(error);
+    AndroidBridge.showToast(`导入失败: ${error.message}`);
+  }
+}
+
+runImportFlow();

+ 8 - 0
resources/AHZYYGZ/adapters.yaml

@@ -0,0 +1,8 @@
+adapters: 
+  - adapter_id: "AHZYYGZ_01" 
+    adapter_name: "安徽中医药高等专科学校" 
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "AHZYYGZ.js" 
+    import_url: "https://jwxt.ahzyygz.edu.cn/ahzyygzjw/frame/homes.action?v=57985306490269832436839"
+    maintainer: "glxgo"
+    description: "安徽中医药高等专科学校青果教务适配器,非本校开发者适配,如果有误建议提交issues"

+ 224 - 0
resources/CFEC/CFEC.js

@@ -0,0 +1,224 @@
+const BASE = `${window.location.origin}/jwglxt`;
+const INDEX_PATH = '/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default';
+const COURSE_API_PATH = '/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151';
+const TIME_API_PATH = '/kbcx/xskbcx_cxRjc.html?gnmkdm=N2151';
+
+async function req(url, method = 'GET', body) {
+  const res = await fetch(url, {
+    method,
+    credentials: 'include',
+    headers: {
+      'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
+      'x-requested-with': 'XMLHttpRequest',
+      'accept': '*/*'
+    },
+    body
+  });
+  if (!res.ok) throw new Error(`请求失败: ${res.status}`);
+  return await res.text();
+}
+
+function isOnTimetablePage() {
+  return window.location.pathname === '/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html';
+}
+
+function readCurrentPageTerm() {
+  const xnmEl = document.querySelector('#xnm');
+  const xqmEl = document.querySelector('#xqm');
+  const xnm = xnmEl ? String(xnmEl.value || '').trim() : '';
+  const xqm = xqmEl ? String(xqmEl.value || '').trim() : '';
+  if (!xnm || !xqm) throw new Error('当前课表页未读到学年学期,请先选择后再导入');
+  return { xnm, xqm };
+}
+
+function parseSelectOptions(selectEl) {
+  if (!selectEl) return { options: [], defaultIndex: 0 };
+  const options = [];
+  let defaultIndex = 0;
+  Array.from(selectEl.querySelectorAll('option')).forEach((opt) => {
+    const value = String(opt.value || '').trim();
+    if (!value) return;
+    const text = String(opt.textContent || '').trim() || value;
+    if (opt.selected) defaultIndex = options.length;
+    options.push({ value, text });
+  });
+  return { options, defaultIndex };
+}
+
+function parseTermOptionsFromDoc(doc) {
+  const yearData = parseSelectOptions(doc.querySelector('#xnm'));
+  const semesterData = parseSelectOptions(doc.querySelector('#xqm'));
+  if (!yearData.options.length || !semesterData.options.length) {
+    throw new Error('课表页学年学期选项解析失败');
+  }
+  return { yearData, semesterData };
+}
+
+async function fetchIndexDoc() {
+  const html = await fetch(`${BASE}${INDEX_PATH}`, { credentials: 'include' }).then(res => {
+    if (!res.ok) throw new Error(`课表页请求失败: ${res.status}`);
+    return res.text();
+  });
+  return new DOMParser().parseFromString(html, 'text/html');
+}
+
+async function selectTermByUserFromDoc(doc) {
+  const { yearData, semesterData } = parseTermOptionsFromDoc(doc);
+
+  const yearIndex = await window.AndroidBridgePromise.showSingleSelection(
+    '选择学年',
+    JSON.stringify(yearData.options.map(item => item.text)),
+    yearData.defaultIndex
+  );
+  if (yearIndex === null || yearIndex === -1) throw new Error('已取消学年选择');
+
+  const semesterIndex = await window.AndroidBridgePromise.showSingleSelection(
+    '选择学期',
+    JSON.stringify(semesterData.options.map(item => item.text)),
+    semesterData.defaultIndex
+  );
+  if (semesterIndex === null || semesterIndex === -1) throw new Error('已取消学期选择');
+
+  return {
+    xnm: yearData.options[yearIndex].value,
+    xqm: semesterData.options[semesterIndex].value
+  };
+}
+
+async function resolveTerm() {
+  if (isOnTimetablePage()) {
+    return readCurrentPageTerm();
+  }
+  const doc = await fetchIndexDoc();
+  return await selectTermByUserFromDoc(doc);
+}
+
+function parseWeeks(zcd) {
+  if (!zcd) return [];
+  const result = new Set();
+  String(zcd).replace(/\s+/g, '').split(/[,,]/).forEach((seg) => {
+    const odd = seg.includes('单');
+    const even = seg.includes('双');
+    const normalized = seg.replace(/周|\(|\)|单|双/g, '');
+    const match = normalized.match(/(\d+)(?:-(\d+))?/);
+    if (!match) return;
+    const start = Number(match[1]);
+    const end = Number(match[2] || match[1]);
+    for (let week = start; week <= end; week++) {
+      if (odd && week % 2 === 0) continue;
+      if (even && week % 2 !== 0) continue;
+      result.add(week);
+    }
+  });
+  return [...result].sort((a, b) => a - b);
+}
+
+function parseCourses(data) {
+  if (!data || !Array.isArray(data.kbList)) {
+    return { courses: [], xqhId: '1' };
+  }
+
+  const courses = [];
+  let xqhId = '1';
+
+  data.kbList.forEach((course) => {
+    if (course.xqh_id) xqhId = String(course.xqh_id).trim() || xqhId;
+
+    const day = Number(course.xqj);
+    const secRaw = String(course.jcs || course.jc || '').replace(/节/g, '').trim();
+    const sectionNums = (secRaw.match(/\d+/g) || []).map(Number).filter(n => !Number.isNaN(n));
+    const weeks = parseWeeks(course.zcd);
+
+    if (!course.kcmc || !sectionNums.length || !weeks.length || !(day >= 1 && day <= 7)) return;
+
+    courses.push({
+      name: String(course.kcmc).trim(),
+      teacher: String(course.xm || '未知').trim(),
+      position: String(course.cdmc || course.cdbh || '未排地点').trim(),
+      day,
+      startSection: sectionNums[0],
+      endSection: sectionNums[sectionNums.length - 1],
+      weeks
+    });
+  });
+
+  const deduped = new Map();
+  courses.forEach((course) => {
+    const key = `${course.name}|${course.teacher}|${course.position}|${course.day}|${course.startSection}|${course.endSection}|${course.weeks.join(',')}`;
+    if (!deduped.has(key)) deduped.set(key, course);
+  });
+
+  return { courses: [...deduped.values()], xqhId };
+}
+
+function parseTimeSlots(data) {
+  if (!Array.isArray(data) || !data.length) throw new Error('未获取到节次时间数据');
+  return data.map((item) => ({
+    number: Number(item.jcmc),
+    startTime: String(item.qssj || '').trim(),
+    endTime: String(item.jssj || '').trim()
+  })).filter(item => item.number > 0 && item.startTime && item.endTime);
+}
+
+async function fetchCourses(xnm, xqm) {
+  const body = `xnm=${encodeURIComponent(xnm)}&xqm=${encodeURIComponent(xqm)}&kzlx=ck&xsdm=&kclbdm=&kclxdm=`;
+  const text = await req(`${BASE}${COURSE_API_PATH}`, 'POST', body);
+  return JSON.parse(text);
+}
+
+async function fetchTimeSlots(xnm, xqm, xqhId) {
+  const body = `xnm=${encodeURIComponent(xnm)}&xqm=${encodeURIComponent(xqm)}&xqh_id=${encodeURIComponent(xqhId || '1')}`;
+  const text = await req(`${BASE}${TIME_API_PATH}`, 'POST', body);
+  return parseTimeSlots(JSON.parse(text));
+}
+
+function validateSemesterStartDateInput(input) {
+  const value = String(input || '').trim();
+  if (!value) return false;
+  return /^\d{4}-\d{2}-\d{2}$/.test(value) ? false : '请输入 YYYY-MM-DD,例如 2026-02-24';
+}
+
+async function selectSemesterStartDate(xnm, xqm) {
+  const defaultDate = xqm === '3' ? `${xnm}-09-01` : `${Number(xnm) + 1}-03-01`;
+  const picked = await window.AndroidBridgePromise.showPrompt(
+    '选择开学日期',
+    '请输入开学日期(YYYY-MM-DD)',
+    defaultDate,
+    'validateSemesterStartDateInput'
+  );
+  if (picked === null) return null;
+  const value = String(picked).trim();
+  return value || null;
+}
+
+async function run() {
+  try {
+    const { xnm, xqm } = await resolveTerm();
+    AndroidBridge.showToast('正在解析课表数据...');
+
+    const rawData = await fetchCourses(xnm, xqm);
+    const { courses, xqhId } = parseCourses(rawData);
+    if (!courses.length) throw new Error('未获取到课表数据');
+
+    const semesterStartDate = await selectSemesterStartDate(xnm, xqm);
+    const timeSlots = await fetchTimeSlots(xnm, xqm, xqhId);
+    const allWeeks = courses.flatMap(course => course.weeks);
+    const semesterTotalWeeks = allWeeks.length ? Math.max(...allWeeks) : 20;
+
+    await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({
+      semesterTotalWeeks,
+      semesterStartDate,
+      firstDayOfWeek: 1
+    }));
+    await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+    await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+
+    AndroidBridge.showToast(`导入成功:${courses.length} 门`);
+    AndroidBridge.notifyTaskCompletion();
+  } catch (error) {
+    console.error(error);
+    AndroidBridge.showToast(`导入失败: ${error.message}`);
+  }
+}
+
+run();

+ 8 - 0
resources/CFEC/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+   - adapter_id: "CFEC_01"
+    adapter_name: "重庆财经学院正方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "CFEC.js" #相对路径
+    import_url: "http://jwmis.cfec.edu.cn/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default"
+    maintainer: "glxgo"
+    description: "重庆财经学院正方教务适配器,非本校开发者适配,如果有误建议提交issues"

+ 8 - 0
resources/XATU/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "XATU_01"
+    adapter_name: "西安工业大学"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "myschool.js"
+    import_url: "http://jwgl2018.xatu.edu.cn"
+    maintainer: "晨熯"
+    description: "树维教务系统适配"

+ 294 - 0
resources/XATU/myschool.js

@@ -0,0 +1,294 @@
+// 西安工业大学(http://jwgl2018.xatu.edu.cn) 拾光课程表适配脚本,基于天津农学院适配脚本
+// 本校开发者适配,出现问题请提issues或者提交pr更改,这更加快速
+//感谢XingHeYuZhuan、aryunm、jursin...等的帮助,感谢trae的辅助
+
+function powerSplit(paramsRaw) {
+    const args = [];
+    let current = "";
+    let depth = 0; 
+    let inQuote = false;
+    let quoteChar = "";
+
+    for (let i = 0; i < paramsRaw.length; i++) {
+        let char = paramsRaw[i];
+        if ((char === '"' || char === "'") && (i === 0 || paramsRaw[i - 1] !== '\\')) {
+            if (!inQuote) { inQuote = true; quoteChar = char; }
+            else if (char === quoteChar) { inQuote = false; }
+        }
+        if (!inQuote) {
+            if (char === '(' || char === '[' || char === '{') depth++;
+            if (char === ')' || char === ']' || char === '}') depth--;
+        }
+        if (char === ',' && depth === 0 && !inQuote) {
+            args.push(cleanArg(current));
+            current = "";
+        } else {
+            current += char;
+        }
+    }
+    args.push(cleanArg(current)); 
+    return args;
+}
+
+function cleanArg(s) {
+    s = s.trim();
+    if (s === "null") return null;
+    return s.replace(/^["']|["']$/g, "");
+}
+
+/**
+ * 全局课程合并逻辑
+ */
+function mergeContinuousLessons(lessons) {
+    if (!lessons || lessons.length === 0) return [];
+
+    // 1. 建立基于 (课程名|教师|地点|星期几) 的分组
+    const groups = {};
+    lessons.forEach(l => {
+        const key = `${l.name}|${l.teacher}|${l.position}|${l.day}`;
+        if (!groups[key]) {
+            groups[key] = {
+                name: l.name,
+                teacher: l.teacher,
+                position: l.position,
+                day: l.day,
+                isTeachingBuilding3: l.isTeachingBuilding3,
+                // 假设大学最多 50 周,构建一个:第 N 周对应哪些节次的矩阵
+                weeksMatrix: Array.from({ length: 50 }, () => new Set())
+            };
+        }
+        // 将系统传来的凌乱数据彻底打散,按“周”填入对应的“节”中,Set自动去重
+        if (l.weeks && Array.isArray(l.weeks)) {
+            l.weeks.forEach(w => {
+                if (w >= 0 && w < 50) {
+                    for (let s = l.startSection; s <= l.endSection; s++) {
+                        groups[key].weeksMatrix[w].add(s);
+                    }
+                }
+            });
+        }
+    });
+
+    const merged = [];
+
+    // 2. 根据矩阵重新组装绝对精确的课程块
+    for (const key in groups) {
+        const group = groups[key];
+        const matrix = group.weeksMatrix;
+        
+        // 用于记录相同的“连续节次块”分布在哪些周次
+        // 例如 blockMap["1-2"] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
+        // 例如 blockMap["2-2"] = [10]
+        const blockMap = {};
+
+        for (let w = 0; w < matrix.length; w++) {
+            const sections = Array.from(matrix[w]).sort((a, b) => a - b);
+            if (sections.length === 0) continue;
+
+            // 寻找当前周的连续节次块
+            let start = sections[0];
+            let prev = sections[0];
+
+            for (let i = 1; i < sections.length; i++) {
+                const curr = sections[i];
+                if (curr === prev + 1) {
+                    prev = curr; // 节次连续,继续延伸
+                } else {
+                    // 节次断开,结算上一个块
+                    const blockKey = `${start}-${prev}`;
+                    if (!blockMap[blockKey]) blockMap[blockKey] = [];
+                    blockMap[blockKey].push(w);
+                    
+                    // 开启新块
+                    start = curr;
+                    prev = curr;
+                }
+            }
+            // 结算每周最后一个块
+            const blockKey = `${start}-${prev}`;
+            if (!blockMap[blockKey]) blockMap[blockKey] = [];
+            blockMap[blockKey].push(w);
+        }
+
+        // 3. 将聚合好的 blockMap 转换为最终的 JSON 对象
+        for (const blockKey in blockMap) {
+            const [startSec, endSec] = blockKey.split('-').map(Number);
+            merged.push({
+                name: group.name,
+                teacher: group.teacher,
+                position: group.position,
+                day: group.day,
+                startSection: startSec,
+                endSection: endSec,
+                weeks: blockMap[blockKey],
+                isTeachingBuilding3: group.isTeachingBuilding3
+            });
+        }
+    }
+
+    // 4. 排序以便输出整洁美观
+    merged.sort((a, b) => {
+        if (a.day !== b.day) return a.day - b.day;
+        if (a.startSection !== b.startSection) return a.startSection - b.startSection;
+        return a.name.localeCompare(b.name);
+    });
+
+    return merged;
+}
+
+function isTeachingBuilding3(position) {
+    return /教3/.test(position);
+}
+
+function parseTaskActivities(html) {
+    const rawResults = [];
+    const blocks = html.split(/var\s+teachers\s*=/);
+
+    for (let i = 1; i < blocks.length; i++) {
+        const block = blocks[i];
+        let teacherName = "未知教师";
+        const tMatch = block.match(/actTeachers\s*=\s*\[\s*\{[\s\S]*?name:\s*"(.*?)"/);
+        if (tMatch) teacherName = tMatch[1];
+
+        const activityMatch = block.match(/new\s+TaskActivity\(([\s\S]*?)\);/);
+        if (!activityMatch) continue;
+
+        const args = powerSplit(activityMatch[1]);
+        const courseName = (args[3] || "未知课程").split('(')[0];
+        const position = (args[5] || "未知地点").replace(/\(.*?\)/g, "");
+        const weeksBitmap = args[6] || "";
+        
+        const weeks = [];
+        for (let j = 0; j < weeksBitmap.length; j++) {
+            if (weeksBitmap[j] === '1') weeks.push(j);
+        }
+
+        const unitCountMatch = html.match(/unitCount\s*=\s*(\d+)/);
+        const unitCount = unitCountMatch ? parseInt(unitCountMatch[1]) : 14;
+
+        const idxRegex = /index\s*=\s*(\d+)\s*\*\s*unitCount\s*\+\s*(\d+);/g;
+        let m;
+        while ((m = idxRegex.exec(block)) !== null) {
+            const day = parseInt(m[1]) + 1; 
+            const section = parseInt(m[2]) + 1;
+
+            rawResults.push({
+                "name": courseName,
+                "teacher": teacherName,
+                "position": position,
+                "day": day,
+                "startSection": section,
+                "endSection": section,
+                "weeks": weeks,
+                "isTeachingBuilding3": isTeachingBuilding3(position)
+            });
+        }
+    }
+
+    // 执行全局合并逻辑
+    return mergeContinuousLessons(rawResults);
+}
+
+async function request(url, options = {}) {
+    const res = await fetch(url, { credentials: "include", ...options });
+    if (!res.ok) throw new Error(`网络请求失败: ${res.status}`);
+    return await res.text();
+}
+
+async function detectParameters() {
+    const html = await request("http://jwgl2018.xatu.edu.cn/eams/courseTableForStd.action?sf_request_type=ajax");
+    const idsMatch = html.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/);
+    const tagIdMatch = html.match(/id="(semesterBar\d+Semester)"/);
+    if (!idsMatch || !tagIdMatch) return null;
+    return { ids: idsMatch[1], tagId: tagIdMatch[1] };
+}
+
+async function getSelectedSemester(tagId) {
+    const raw = await request(`http://jwgl2018.xatu.edu.cn/eams/dataQuery.action?sf_request_type=ajax`, {
+        method: "POST",
+        headers: { "Content-Type": "application/x-www-form-urlencoded" },
+        body: `tagId=${encodeURIComponent(tagId)}&dataType=semesterCalendar`
+    });
+    const data = Function(`return (${raw});`)();
+    const list = [];
+    for (let key in data.semesters) {
+        data.semesters[key].forEach(s => list.push({ id: s.id, name: `${s.schoolYear} ${s.name}学期` }));
+    }
+    const idx = await window.AndroidBridgePromise.showSingleSelection("选择学期", JSON.stringify(list.map(s => s.name)), -1);
+    return idx !== null ? list[idx] : null;
+}
+
+async function fetchAndParseCourses(semesterId, ids) {
+    const html = await request(`http://jwgl2018.xatu.edu.cn/eams/courseTableForStd!courseTable.action?sf_request_type=ajax`, {
+        method: "POST",
+        headers: { "Content-Type": "application/x-www-form-urlencoded" },
+        body: `ignoreHead=1&setting.kind=std&semester.id=${semesterId}&ids=${ids}`
+    });
+    return parseTaskActivities(html);
+}
+
+async function applyTimeSlots() {
+    const slots = [
+        { "number": 1, "startTime": "08:20", "endTime": "09:05" }, 
+        { "number": 2, "startTime": "09:15", "endTime": "10:00" },
+        { "number": 3, "startTime": "10:20", "endTime": "11:05" },
+        { "number": 4, "startTime": "11:15", "endTime": "12:00" },
+        { "number": 5, "startTime": "14:00", "endTime": "14:45" },
+        { "number": 6, "startTime": "14:55", "endTime": "15:40" },
+        { "number": 7, "startTime": "16:00", "endTime": "16:45" }, 
+        { "number": 8, "startTime": "16:55", "endTime": "17:40" },
+        { "number": 9, "startTime": "18:10", "endTime": "18:55" }, 
+        { "number": 10, "startTime": "19:05", "endTime": "19:50" },
+        { "number": 11, "startTime": "20:00", "endTime": "20:45" },
+        { "number": 11, "startTime": "20:55", "endTime": "21:40" },
+    ];
+    return await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(slots));
+}
+
+
+function adjustTeachingBuilding3Courses(courses) {
+    return courses.map(course => {
+        // 检查是否是教3楼且正好是第3-4节两节课
+        if (course.isTeachingBuilding3 && 
+            course.startSection === 3 && 
+            course.endSection === 4) {
+            // 设置为自定义时间模式
+            course.isCustomTime = true;
+            course.customStartTime = "10:10";
+            course.customEndTime = "11:40";
+        }
+        return course;
+    });
+}
+
+async function runImportFlow() {
+    try {
+        AndroidBridge.showToast("开始探测教务参数...");
+        const params = await detectParameters();
+        if (!params) throw new Error("未能识别教务参数,请确认已登录");
+
+        const semester = await getSelectedSemester(params.tagId);
+        if (!semester) return; 
+
+        AndroidBridge.showToast("正在同步课表...");
+        let courses = await fetchAndParseCourses(semester.id, params.ids);
+        
+        if (!courses || courses.length === 0) throw new Error("未解析到课程数据");
+
+        // 调整教3楼课程时间
+        courses = adjustTeachingBuilding3Courses(courses);
+
+        await applyTimeSlots();
+        const saveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        
+        if (saveResult) {
+            AndroidBridge.showToast(`成功导入 ${courses.length} 个课程条目`);
+            AndroidBridge.notifyTaskCompletion();
+        }
+    } catch (e) {
+        console.error(`[异常] ${e.message}`);
+        AndroidBridge.showToast(e.message);
+    }
+}
+
+runImportFlow();