// ==UserScript==
// @name 学习通作业/考试/任务列表(优化版)
// @namespace https://github.com/Cooanyh
// @version 2.4.9-beta-1
// @author 甜檸Cirtron (lcandy2); Modified by Coren
// @description 【优化版】支持作业、考试与课程任务快速查看;提供统一设置、任务分类筛选、按课程忽略及任务引擎模块汇总。
// @license AGPL-3.0-or-later
// @copyright lcandy2 All Rights Reserved
// @copyright 2025, Coren (Modified based on original work)
// @icon https://www.google.com/s2/favicons?sz=64&domain=chaoxing.com
// @source https://github.com/Cooanyh/chaoxing-list
// @match *://i.chaoxing.com/*
// @match *://i.mooc.chaoxing.com/space/index*
// @match *://i.mooc.chaoxing.com/settings*
// @match *://mooc2-ans.chaoxing.com/*
// @match *://mooc1-api.chaoxing.com/work/stu-work*
// @match *://mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode*
// @match *://mooc1.chaoxing.com/exam-ans/exam/test/examcode/examlist*
// @require https://registry.npmmirror.com/vue/3.4.27/files/dist/vue.global.prod.js
// @require data:application/javascript,%3Bwindow.Vue%3DVue%3B
// @require https://registry.npmmirror.com/vuetify/3.6.6/files/dist/vuetify.min.js
// @require data:application/javascript,%3B
// @resource VuetifyStyle https://registry.npmmirror.com/vuetify/3.6.6/files/dist/vuetify.min.css
// @resource material-design-icons-iconfont/dist/material-design-icons.css https://fonts.googlefonts.cn/css?family=Material+Icons
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_xmlhttpRequest
// @connect mooc1-api.chaoxing.com
// @connect mobilelearn.chaoxing.com
// @connect stat2-ans.chaoxing.com
// @connect mooc2-ans.chaoxing.com
// @connect mooc1.chaoxing.com
// @connect i.chaoxing.com
// @connect task.chaoxing.com
// @run-at document-end
// ==/UserScript==
(function (vuetify, vue) {
'use strict';
// --- 核心工具函数 ---
const wrapElements = () => {
const wrapper = document.createElement("body");
wrapper.id = "chaoxing-assignment-wrapper";
while (document.body.firstChild) {
wrapper.appendChild(document.body.firstChild);
}
document.body.appendChild(wrapper);
wrapper.style.display = "none";
};
const removeStyles = () => {
removeHtmlStyle();
const styles = document.querySelectorAll("link[rel=stylesheet]");
styles.forEach((style) => {
var _a;
if ((_a = style.getAttribute("href")) == null ? void 0 : _a.includes("chaoxing")) {
style.remove();
}
});
};
const removeHtmlStyle = () => {
const html = document.querySelector("html");
html == null ? void 0 : html.removeAttribute("style");
};
const keepRemoveHtmlStyle = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "attributes" && mutation.attributeName === "style") {
removeHtmlStyle();
}
});
});
const html = document.querySelector("html");
html && observer.observe(html, { attributes: true });
};
const removeScripts = () => {
const scripts = document.querySelectorAll("script");
scripts.forEach((script) => {
var _a;
if ((_a = script.getAttribute("src")) == null ? void 0 : _a.includes("chaoxing")) {
script.remove();
}
});
};
// --- 课程进度查询开关管理 ---
const PROGRESS_ENABLED_KEY = 'chaoxing_progress_enabled';
const PROGRESS_DELAY_KEY = 'chaoxing_progress_delay';
const DEFAULT_PROGRESS_DELAY = 500;
const MIN_PROGRESS_DELAY = 300;
const MAX_PROGRESS_DELAY = 10000;
const isProgressEnabled = () => {
try {
const stored = localStorage.getItem(PROGRESS_ENABLED_KEY);
return stored === null ? true : stored === 'true';
} catch { return true; }
};
const setProgressEnabled = (enabled) => {
try {
localStorage.setItem(PROGRESS_ENABLED_KEY, String(enabled));
console.log('[课程进度] 查询功能已', enabled ? '开启' : '关闭');
} catch (e) {
console.error('[课程进度] 保存开关状态失败:', e);
}
};
const normalizeProgressDelay = (value) => {
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) return DEFAULT_PROGRESS_DELAY;
return Math.min(MAX_PROGRESS_DELAY, Math.max(MIN_PROGRESS_DELAY, parsed));
};
const getProgressDelay = () => {
try {
const stored = localStorage.getItem(PROGRESS_DELAY_KEY);
return stored === null ? DEFAULT_PROGRESS_DELAY : normalizeProgressDelay(stored);
} catch { return DEFAULT_PROGRESS_DELAY; }
};
const setProgressDelay = (delay) => {
const normalizedDelay = normalizeProgressDelay(delay);
try {
localStorage.setItem(PROGRESS_DELAY_KEY, String(normalizedDelay));
console.log('[课程进度] 请求延迟已设置为', normalizedDelay, 'ms');
} catch (e) {
console.error('[课程进度] 保存延迟设置失败:', e);
}
return normalizedDelay;
};
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const getCourseCompletionRate = (item) => {
const rate = Number.parseFloat(String(item?.completionRate ?? '').replace('%', ''));
return Number.isFinite(rate) ? rate : null;
};
const compareCourseProgress = (a, b, direction = 1) => {
const rateA = getCourseCompletionRate(a);
const rateB = getCourseCompletionRate(b);
if (rateA === null && rateB !== null) return 1;
if (rateA !== null && rateB === null) return -1;
if (rateA !== null && rateB !== null && rateA !== rateB) return (rateA - rateB) * direction;
return (a.courseName || '').localeCompare(b.courseName || '', 'zh-CN');
};
// --- 忽略管理 - 使用 localStorage 持久化 ---
const IGNORE_STORAGE_KEY = 'chaoxing_ignored_items';
const SETTINGS_STORAGE_KEY = 'chaoxing_list_settings_v1';
const DASHBOARD_CACHE_STORAGE_KEY = 'chaoxing_list_dashboard_cache_v1';
const DASHBOARD_CACHE_TTL = 5 * 60 * 1000;
const COURSE_IGNORE_PREFIX = 'course__';
const DEFAULT_SETTINGS = Object.freeze({
ignoreEnabled: false,
showOtherTasks: false,
otherTaskTypes: [],
shortTermCacheEnabled: false,
dynamicIgnoreRules: { homework: false, exam: false, activities: false, progress: false },
settingsHintSeen: false,
courseIgnoreHintSeen: false
});
const CORE_TASK_TYPES = new Set(['作业', '视频/任务点', 'AI实践', '任务引擎', '分组讨论', '分组任务', '测验', '随堂练习', '考试']);
const OTHER_TASK_TYPES = ['签到', '问卷', '抢答', '通知', '投票', '直播', '评分', '拍照', '笔记', '其他任务'];
const getSettings = () => {
try {
return { ...DEFAULT_SETTINGS, ...JSON.parse(localStorage.getItem(SETTINGS_STORAGE_KEY) || '{}') };
} catch { return { ...DEFAULT_SETTINGS }; }
};
const saveSettings = (settings) => localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify({ ...DEFAULT_SETTINGS, ...settings }));
const getDashboardCache = () => {
try { return JSON.parse(localStorage.getItem(DASHBOARD_CACHE_STORAGE_KEY) || '{}'); }
catch { return {}; }
};
const cleanupDashboardCache = () => {
const cache = getDashboardCache();
const now = Date.now();
Object.keys(cache).forEach(key => {
if (!cache[key] || !cache[key].savedAt || now - cache[key].savedAt > DASHBOARD_CACHE_TTL) delete cache[key];
});
try { localStorage.setItem(DASHBOARD_CACHE_STORAGE_KEY, JSON.stringify(cache)); } catch { /* localStorage 不可用时直接跳过 */ }
return cache;
};
const saveDashboardCache = (key, data) => {
const cache = cleanupDashboardCache();
cache[key] = { savedAt: Date.now(), data };
try { localStorage.setItem(DASHBOARD_CACHE_STORAGE_KEY, JSON.stringify(cache)); } catch (error) { console.warn('[短时缓存] 写入失败', error); }
};
const isOtherTask = (item) => item && item.isActivity && !CORE_TASK_TYPES.has(item.type);
const isOtherTaskVisible = (item, settings) => !isOtherTask(item) || settings.showOtherTasks || (settings.otherTaskTypes || []).includes(item.type);
// 动态忽略只接受明确的完成/过期标记,避免状态缺失或未知内容被误选。
const isExplicitlyCompletedAiPractice = (item) => item?.type === 'AI实践'
&& (item.finished === true || item.status === '已完成');
const isExplicitlyCompletedHomework = (item) => {
if (!item) return false;
if (item.type === 'AI实践') return isExplicitlyCompletedAiPractice(item);
return item.uncommitted === false || item.finished === true || item.status === '已完成';
};
const isExplicitlyClosedExam = (item) => item?.finished === true || item?.expired === true;
const isExplicitlyEndedActivity = (item) => item?.finished === true || item?.status === '已结束';
const isExplicitlyCompletedProgress = (item) => item?.isComplete === true || Number.parseInt(item?.completionRate, 10) >= 100;
const getTaskEnginePlanSection = (item) => {
if (!item?.taskEnginePlan) return 'activities';
if (item.type === '作业') return 'homework';
if (item.type === '考试') return 'exam';
return 'activities';
};
const isDynamicallyIgnored = (item, sectionType, settings) => {
const rules = settings.dynamicIgnoreRules || {};
// 待办中的任务引擎子任务按实际类目应用规则,其余课程任务沿用课程任务规则。
if (sectionType === 'todo' && item.isActivity) sectionType = getTaskEnginePlanSection(item);
switch (sectionType) {
case 'homework': return !!rules.homework && isExplicitlyCompletedHomework(item);
case 'exam': return !!rules.exam && isExplicitlyClosedExam(item);
case 'activities': return !!rules.activities && isExplicitlyEndedActivity(item);
case 'progress': return !!rules.progress && isExplicitlyCompletedProgress(item);
default: return false;
}
};
const getCourseIgnoreKey = (item) => `${COURSE_IGNORE_PREFIX}${item.courseId || ''}__${item.clazzId || item.classId || ''}`;
const isCourseIgnored = (item) => {
const courseId = item && item.courseId;
if (!courseId) return false;
return !!getIgnoredItems()[getCourseIgnoreKey(item)];
};
const ignoreCourse = (course) => {
const ignored = getIgnoredItems();
ignored[getCourseIgnoreKey(course)] = {
title: course.courseName || course.course || '未知课程',
course: course.courseName || course.course || '',
sectionType: 'course',
ignoredAt: Date.now(),
courseId: course.courseId,
clazzId: course.clazzId || course.classId
};
saveIgnoredItems(ignored);
};
const unignoreCourse = (course) => {
const ignored = getIgnoredItems();
delete ignored[getCourseIgnoreKey(course)];
saveIgnoredItems(ignored);
};
const getIgnoredItems = () => {
try {
return JSON.parse(localStorage.getItem(IGNORE_STORAGE_KEY) || '{}');
} catch { return {}; }
};
const saveIgnoredItems = (items) => {
localStorage.setItem(IGNORE_STORAGE_KEY, JSON.stringify(items));
};
// 生成唯一标识(包含板块前缀,防止不同板块同名项冲突)
const getItemKey = (item, sectionType) => {
const sec = sectionType || item._sectionType || 'unknown';
const id = item.courseId || item.examId || item.workId || item.activeId || '';
const title = item.title || item.courseName || '';
return `${sec}__${id}__${title}`;
};
const isItemIgnored = (item, sectionType) => {
const ignored = getIgnoredItems();
return isCourseIgnored(item) || !!ignored[getItemKey(item, sectionType)];
};
const ignoreItem = (item, sectionType) => {
const ignored = getIgnoredItems();
const sec = sectionType || item._sectionType || 'unknown';
ignored[getItemKey(item, sectionType)] = {
title: item.title || item.courseName || '',
type: item.type || '',
course: item.course || item.courseName || '',
sectionType: sec,
ignoredAt: Date.now()
};
saveIgnoredItems(ignored);
};
const unignoreItem = (item, sectionType) => {
const ignored = getIgnoredItems();
delete ignored[getItemKey(item, sectionType)];
saveIgnoredItems(ignored);
};
const ignoreItems = (items, sectionType) => {
const ignored = getIgnoredItems();
items.forEach(item => {
const sec = sectionType || item._sectionType || 'unknown';
ignored[getItemKey(item, sectionType)] = {
title: item.title || item.courseName || '',
type: item.type || '',
course: item.course || item.courseName || '',
sectionType: sec,
ignoredAt: Date.now()
};
});
saveIgnoredItems(ignored);
};
const unignoreItems = (items, sectionType) => {
const ignored = getIgnoredItems();
items.forEach(item => {
delete ignored[getItemKey(item, sectionType)];
});
saveIgnoredItems(ignored);
};
// 获取某板块的所有已忽略项(按 sectionType 过滤)
const getIgnoredItemsBySection = (sectionType) => {
const ignored = getIgnoredItems();
const result = [];
for (const [key, val] of Object.entries(ignored)) {
if ((val.sectionType || 'unknown') === sectionType) {
result.push({ ...val, _key: key });
}
}
return result;
};
const unignoreByKey = (key) => {
const ignored = getIgnoredItems();
delete ignored[key];
saveIgnoredItems(ignored);
};
const unignoreByKeys = (keys) => {
const ignored = getIgnoredItems();
keys.forEach(k => delete ignored[k]);
saveIgnoredItems(ignored);
};
const urlDetection = () => {
const url = window.location.href;
const hash = window.location.hash;
// 新版仪表盘检测
if (hash.includes("chaoxing-dashboard")) {
return "dashboard";
}
if (hash.includes("chaoxing-assignment-activities")) {
return "activities";
}
if (hash.includes("chaoxing-assignment-todo")) {
return "todo";
}
if (hash.includes("chaoxing-assignment")) {
if (url.includes("mooc1-api.chaoxing.com/work/stu-work")) {
return "homework";
}
if (url.includes("mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode")) {
return "exam";
}
}
if (url.includes("mooc1.chaoxing.com/exam-ans/exam/test/examcode/examlist")) {
return "exam";
}
if (url.includes("i.chaoxing.com")) {
return "home";
}
if (url.includes("i.mooc.chaoxing.com/space/index") || url.includes("i.mooc.chaoxing.com/settings")) {
return "legacyHome";
}
// 课程章节页面检测
if (url.includes("mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentcourse") ||
url.includes("mooc2-ans.chaoxing.com/mooc2-ans/mycourse/stu")) {
return "course_chapter";
}
};
const fixCssConflict = () => {
const style = document.createElement('style');
style.textContent = `
.menu-list .label-item h3 {
font-size: 14px !important;
margin: 0 !important;
line-height: 24px !important;
font-weight: normal !important;
}
div.menubar a h5 {
font-size: 14px !important;
margin: 0 !important;
line-height: normal !important;
font-weight: bold !important;
white-space: nowrap !important;
}
.leftnav h3, .left_nav h3, .funclistul h3, .user-info h3,
div[class*="menu"] h3, div[class*="nav"] h3, #space_left h3, .space-left h3 {
font-size: 14px !important;
}
.space_opt .manageBtn {
font-size: 12px !important;
line-height: 24px !important;
height: auto !important;
width: auto !important;
padding: 0 10px !important;
margin: 0 5px !important;
display: inline-block !important;
box-sizing: content-box !important;
text-align: center !important;
white-space: nowrap !important;
border-radius: 4px !important;
}
.space_opt a.manageBtn {
text-decoration: none !important;
color: #333 !important;
}
`;
document.head.appendChild(style);
};
const createMenuItem = (id, text, iconClass, targetUrl, insertFunc) => {
const url = targetUrl;
const menubarElement = document.querySelector('div.menubar[role="menubar"]');
if (menubarElement) {
const a = document.createElement("a");
a.setAttribute("role", "menuitem");
a.setAttribute("tabindex", "-1");
a.id = `first${id}`;
a.setAttribute("onclick", `setUrl('${id}','${url}',this,'0','${text}')`);
a.setAttribute("dataurl", url);
const icon = document.createElement("span");
icon.className = `icon-space ${iconClass}`;
a.appendChild(icon);
const h5 = document.createElement("h5");
h5.title = text;
h5.innerHTML = `${text}`;
a.appendChild(h5);
const arrow = document.createElement("span");
arrow.className = "arrow icon-uniE900";
a.appendChild(arrow);
if (insertFunc) insertFunc(menubarElement, a);
else menubarElement.prepend(a);
}
};
const createMenuItemNew = (id, text, iconClass, targetUrl, insertFunc) => {
const menuListElement = document.querySelector("ul.menu-list-ul");
if (menuListElement) {
const li = document.createElement("li");
li.setAttribute("level", "1");
li.setAttribute("table-type", "1");
li.setAttribute("data-id", `chaoxing-assignment-${id}`);
const div = document.createElement("div");
div.className = "label-item";
div.setAttribute("role", "menuitem");
div.setAttribute("level", "1");
div.setAttribute("tabindex", "-1");
div.setAttribute("name", text);
div.setAttribute("id", `first_chaoxing_assignment_${id}`);
div.setAttribute("onclick", `setUrl('chaoxing-assignment-${id}','${targetUrl}',this,'0','${text}')`);
div.setAttribute("dataurl", targetUrl);
const icon = document.createElement("span");
icon.className = `icon-space ${iconClass}`;
div.appendChild(icon);
const h3 = document.createElement("h3");
h3.title = text;
h3.textContent = text;
div.appendChild(h3);
const arrow = document.createElement("span");
arrow.className = "slide-arrow icon-h-arrow-l hide";
div.appendChild(arrow);
li.appendChild(div);
li.appendChild(Object.assign(document.createElement("div"), { className: "school-level" })).appendChild(document.createElement("ul"));
if (insertFunc) insertFunc(menuListElement, li);
else menuListElement.prepend(li);
}
};
const createMenuItemLegacy = (id, text, iconClass, targetUrl) => {
const list = document.querySelector("ul.funclistul");
if (list && !document.querySelector(`#li_chaoxing-assignment-${id}`)) {
const li = document.createElement("li");
li.id = `li_chaoxing-assignment-${id}`;
li.className = '';
li.innerHTML = `${text}`;
list.prepend(li);
}
};
// URL 常量
const URL_HOMEWORK = 'https://mooc1-api.chaoxing.com/work/stu-work#chaoxing-assignment';
const URL_EXAM = 'https://mooc1-api.chaoxing.com/exam-ans/exam/phone/examcode#chaoxing-assignment';
const URL_TODO = 'https://mooc1-api.chaoxing.com/work/stu-work#chaoxing-assignment-todo';
const URL_ACTIVITIES = 'https://mooc1-api.chaoxing.com/work/stu-work#chaoxing-assignment-activities';
const URL_DASHBOARD = 'https://mooc1-api.chaoxing.com/work/stu-work#chaoxing-dashboard';
const API_COURSE_LIST = 'https://mooc1-api.chaoxing.com/mycourse/backclazzdata?view=json&mcode=';
const initMenus = () => {
// 只创建单个"学习仪表盘"入口
if (document.querySelector('div.menubar[role="menubar"]')) {
if (!document.querySelector('#first1000000')) {
createMenuItem('1000000', '📊 学习仪表盘', 'icon-bj', URL_DASHBOARD);
}
}
else if (document.querySelector("ul.menu-list-ul")) {
if (!document.querySelector('#first_chaoxing_assignment_dashboard')) {
createMenuItemNew('dashboard', '📊 学习仪表盘', 'icon-bj', URL_DASHBOARD);
}
}
else if (document.querySelector("ul.funclistul")) {
createMenuItemLegacy('dashboard', '📊 学习仪表盘', 'zne_bj_icon', URL_DASHBOARD);
}
};
// 新标签页打开的菜单项创建函数
const createMenuItemNewTab = (id, text, iconClass, targetUrl) => {
const menubarElement = document.querySelector('div.menubar[role="menubar"]');
if (menubarElement) {
const a = document.createElement("a");
a.setAttribute("role", "menuitem");
a.setAttribute("tabindex", "-1");
a.id = `first${id}`;
a.href = targetUrl;
a.target = "_blank";
a.style.cursor = "pointer";
const icon = document.createElement("span");
icon.className = `icon-space ${iconClass}`;
a.appendChild(icon);
const h5 = document.createElement("h5");
h5.title = text;
h5.innerHTML = `${text}`;
a.appendChild(h5);
const arrow = document.createElement("span");
arrow.className = "arrow icon-uniE900";
a.appendChild(arrow);
menubarElement.prepend(a);
}
};
const createMenuItemNewTabNew = (id, text, iconClass, targetUrl) => {
const menuListElement = document.querySelector("ul.menu-list-ul");
if (menuListElement) {
const li = document.createElement("li");
li.setAttribute("level", "1");
li.setAttribute("table-type", "1");
li.setAttribute("data-id", `chaoxing-assignment-${id}`);
const div = document.createElement("div");
div.className = "label-item";
div.setAttribute("role", "menuitem");
div.setAttribute("level", "1");
div.setAttribute("tabindex", "-1");
div.setAttribute("name", text);
div.setAttribute("id", `first_chaoxing_assignment_${id}`);
div.style.cursor = "pointer";
div.onclick = () => window.open(targetUrl, '_blank');
const icon = document.createElement("span");
icon.className = `icon-space ${iconClass}`;
div.appendChild(icon);
const h3 = document.createElement("h3");
h3.title = text;
h3.textContent = text;
div.appendChild(h3);
const arrow = document.createElement("span");
arrow.className = "slide-arrow icon-h-arrow-l hide";
div.appendChild(arrow);
li.appendChild(div);
li.appendChild(Object.assign(document.createElement("div"), { className: "school-level" })).appendChild(document.createElement("ul"));
menuListElement.prepend(li);
}
};
const createMenuItemLegacyNewTab = (id, name, iconClass, url) => {
const list = document.querySelector("ul.funclistul");
if (list && !document.querySelector(`#first_chaoxing_assignment_${id}`)) {
const existingItem = list.querySelector('li a');
if (existingItem) {
const li = document.createElement("li");
li.id = `first_chaoxing_assignment_${id}`;
const a = document.createElement("a");
a.href = url;
a.target = "_blank";
a.title = name;
const span = document.createElement("span");
span.className = iconClass;
a.appendChild(span);
a.appendChild(document.createTextNode(name));
li.appendChild(a);
list.prepend(li);
}
}
};
// --- 课程任务汇总功能 ---
// GM_xmlhttpRequest 封装为 Promise
const gmFetch = (url, options = {}) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: options.method || 'GET',
url: url,
headers: options.headers || {},
data: options.data,
responseType: options.responseType || 'text',
timeout: options.timeout || 15000,
onload: (response) => {
if (response.status >= 200 && response.status < 400) {
resolve(response);
} else {
reject(new Error(`Request failed: ${response.status}`));
}
},
onerror: (error) => reject(error),
ontimeout: () => reject(new Error('Request timeout'))
});
});
};
// 获取所有课程列表
const fetchCourseList = async () => {
try {
console.log('[课程任务] 正在获取课程列表:', API_COURSE_LIST);
const response = await gmFetch(API_COURSE_LIST);
console.log('[课程任务] 课程列表原始响应:', response.responseText.substring(0, 1000));
const data = JSON.parse(response.responseText);
console.log('[课程任务] 解析后数据:', data);
if (!data.channelList) {
console.log('[课程任务] 没有 channelList');
return [];
}
const courses = [];
for (const channel of data.channelList) {
const content = channel.content;
if (!content) continue;
// 检查是否是课程(有 course 对象)
if (content.course && content.course.data && content.course.data.length > 0) {
const courseInfo = content.course.data[0];
// 尝试多种方式获取 clazzId
let clazzId = '';
if (content.clazz && content.clazz.data && content.clazz.data.length > 0) {
clazzId = String(content.clazz.data[0].id);
} else if (content.id) {
clazzId = String(content.id);
} else if (channel.key) {
clazzId = String(channel.key);
}
// 只添加有 clazzId 的课程(API 需要此参数)
if (courseInfo && clazzId) {
courses.push({
courseId: String(courseInfo.id),
courseName: courseInfo.name || '未知课程',
clazzId: clazzId,
cpi: String(content.cpi || ''),
teacherName: courseInfo.teacherfactor || ''
});
console.log(`[课程任务] 解析课程: ${courseInfo.name}, clazzId=${clazzId}`);
} else if (courseInfo) {
console.log(`[课程任务] 跳过无 clazzId 的课程: ${courseInfo.name}`);
}
}
}
console.log('[课程任务] 最终解析到课程:', courses.length, '个');
return courses;
} catch (error) {
console.error('[课程任务] 获取课程列表失败:', error);
return [];
}
};
const normalizeTaskType = (item) => {
const typeMap = {
0: '签到', 2: '签到', 4: '抢答', 5: '分组讨论', 6: '投票',
14: '问卷', 17: '直播', 23: '随堂练习', 35: '分组任务', 42: '随堂练习',
43: '评分', 45: '拍照', 47: '作业', 64: '笔记'
};
const title = `${item.nameOne || item.name || item.title || ''}`;
if (/AI\s*实践|AI\s*评价/i.test(title)) return 'AI实践';
if (/分组/.test(title) && /讨论/.test(title)) return '分组讨论';
if (/考试/.test(title)) return '考试';
if (/测验|练习/.test(title)) return '测验';
return typeMap[item.activeType] || typeMap[item.type] || '其他任务';
};
const getTaskEngineLink = (taskId, course) => taskId
? `https://task.chaoxing.com/api/v1/middlePageApi/jumpStudyPlanList?taskId=${encodeURIComponent(taskId)}&moocClassId=${encodeURIComponent(course.clazzId)}`
: '';
const normalizeTaskEnginePlanType = (plan) => {
const typeName = `${plan.planTypeName || ''}`.trim();
const searchable = `${typeName} ${plan.name || ''}`;
if (/AI\s*实践|AI\s*评价|AI\s*对话/i.test(searchable)) return 'AI实践';
if (/分组/.test(searchable) && /(任务|作业)/.test(searchable)) return '分组任务';
if (/讨论/.test(searchable)) return '分组讨论';
if (/考试/.test(searchable)) return '考试';
if (/作业/.test(searchable)) return '作业';
if (/随堂练习/.test(searchable)) return '随堂练习';
if (/测验|自测|练习/.test(searchable)) return '测验';
if (/签到/.test(searchable)) return '签到';
if (/问卷/.test(searchable)) return '问卷';
if (/抢答/.test(searchable)) return '抢答';
if (/投票/.test(searchable)) return '投票';
if (/直播/.test(searchable)) return '直播';
if (/评分/.test(searchable)) return '评分';
if (/拍照/.test(searchable)) return '拍照';
if (/笔记/.test(searchable)) return '笔记';
if (/课程|章节|知识点|视频|文档|音频|阅读/.test(searchable)) return '视频/任务点';
const typeMap = {
0: '视频/任务点', 1: '视频/任务点', 4: '作业', 5: '考试',
8: '视频/任务点', 9: '签到', 10: '视频/任务点', 11: '视频/任务点',
13: '测验', 14: '分组讨论', 15: 'AI实践', 17: '测验', 22: '视频/任务点'
};
return typeMap[plan.planType] || '其他任务';
};
const normalizeTaskEngineSummaryItem = (item, course) => {
const planCount = Number(item.planCount) || 0;
const planFinishCount = Number(item.planFinishCount) || 0;
const studyProgress = Number(item.taskStudyProgress) || 0;
const qualifyStatus = `${item.userTaskQualifyStatus || ''}`;
const isFinished = qualifyStatus === '已达标'
|| qualifyStatus === '已完成'
|| studyProgress >= 1
|| (planCount > 0 && planFinishCount >= planCount);
const taskId = item.id ? String(item.id) : '';
return {
activeId: taskId ? `task-engine-${taskId}` : `task-engine-${course.courseId}-${item.name || ''}`,
taskId,
title: item.name || '未命名任务引擎任务',
type: '任务引擎',
status: isFinished ? '已结束' : '进行中',
time: item.lastStudyDateStr || item.createDate || '',
endTime: '',
courseName: course.courseName,
course: course.courseName,
courseId: course.courseId,
clazzId: course.clazzId,
cpi: course.cpi,
finished: isFinished,
ongoing: !isFinished,
activeType: 'task-engine',
taskEngine: true,
taskEnginePlan: false,
taskLink: getTaskEngineLink(taskId, course),
progressText: planCount > 0 ? `${planFinishCount}/${planCount}` : '',
isActivity: true
};
};
const extractTaskEngineUserId = (response) => {
const html = `${response?.responseText || ''}`;
const htmlMatch = html.match(/(?:const|let|var)\s+eTaskUserId\s*=\s*["']([^"']+)["']/);
if (htmlMatch) return htmlMatch[1];
try {
return new URL(response?.finalUrl || '').searchParams.get('encryTaskUserId') || '';
} catch {
return '';
}
};
const parseTaskEngineDate = (value) => {
if (value === null || value === undefined || value === '') return NaN;
if (typeof value === 'number') return Number.isFinite(value) ? value : NaN;
const text = normalizeTaskEngineDateText(value);
// 平台详情页常返回“09-20 20:00”这种省略年份的日期。
// Date.parse 会把它误解析为 2001 年,进而把当前年度的任务判为已过期。
const shortDateMatch = text.match(/^(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
if (shortDateMatch) {
const [, monthText, dayText, hourText = '0', minuteText = '0', secondText = '0'] = shortDateMatch;
const now = new Date();
const year = now.getFullYear();
const parsed = new Date(
year,
Number(monthText) - 1,
Number(dayText),
Number(hourText),
Number(minuteText),
Number(secondText)
);
if (
parsed.getFullYear() === year
&& parsed.getMonth() === Number(monthText) - 1
&& parsed.getDate() === Number(dayText)
) return parsed.getTime();
}
return Date.parse(text.replace(/-/g, '/'));
};
const isTaskEnginePlanFinished = (plan) => plan.isFinish === true
|| Number(plan.isFinish) === 1
|| Number(plan.planUser?.finish) === 1
|| Number(plan.score?.completed) === 1;
const taskEngineDetailCache = new Map();
const TASK_ENGINE_DEADLINE_TYPES = new Set([
'作业', '考试', '测验', '随堂练习', '分组任务', '分组讨论',
'签到', '问卷', '抢答', '投票', '直播', 'AI实践'
]);
const TASK_ENGINE_DETAIL_HOSTS = new Set([
'mooc1.chaoxing.com', 'mooc2-ans.chaoxing.com', 'mobilelearn.chaoxing.com',
'i.chaoxing.com', 'task.chaoxing.com'
]);
const htmlToTaskEngineText = (html) => {
const source = `${html || ''}`;
if (typeof DOMParser !== 'undefined') {
const doc = new DOMParser().parseFromString(source, 'text/html');
return `${doc.body?.textContent || ''}`.replace(/\s+/g, ' ').trim();
}
return source
.replace(/