// ==UserScript==
// @name 2026年暑期教师研修|国家智慧教育公共服务平台|国家职教与高教智慧教育平台|自动刷视频自动播放(职教/高教)
// @namespace local.codex.smartedu
// @version 2026.07.29.3
// @description 适配2026年暑期教师研修职教/高教专题:从未达到认定学时的课程和未完成视频处续播。
// @author local
// @match https://www.smartedu.cn/jiaoshi/training/595a8948-d059-4972-bc30-bf10685f0061*
// @match https://www.smartedu.cn/jiaoshi/courseIndex*
// @match https://www.smartedu.cn/jiaoshi/courseDetail*
// @match https://teacher.ykt.eduyun.cn/teacherTraining/courseIndex*
// @match https://teacher.ykt.eduyun.cn/teacherTraining/courseDetail*
// @run-at document-idle
// @grant none
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const TRAINING_ID = "595a8948-d059-4972-bc30-bf10685f0061";
const TRAINING_URL = `https://www.smartedu.cn/jiaoshi/training/${TRAINING_ID}`;
const COURSE_BASE_URL = "https://www.smartedu.cn/jiaoshi/courseIndex";
const COURSE_CONTEXT = {
tag: "2026年“暑期教师研修”专题(职业教育、高等教育)",
channelId: "",
libraryId: "bb042e69-9a11-49a1-af22-0c3fab2e92b9",
breadcrumb: "2026年“暑期教师研修”专题(职业教育、高等教育)",
};
// 职教、高教使用同一专题课程清单,无需再选择方向。
const COURSES = [
{
id: "2e001e74-34e1-4b0f-bda9-5350799a91fc",
name: "大力弘扬教育家精神",
recognizedHours: 2,
totalSeconds: 160749,
activitySetId: "fafbb4a4-a148-4d63-a55d-d8afaa11a6cb",
},
{
id: "058fa244-f310-41e8-a73b-2853030db536",
name: "综合育人能力提升",
recognizedHours: 4,
totalSeconds: 82814,
activitySetId: "63d1876a-e104-4fd0-b9b8-ee661c1a2597",
},
{
id: "b9141681-d108-45d9-8891-d99fe7b3289b",
name: "职教“双师”能力提升",
recognizedHours: 4,
totalSeconds: 49131,
activitySetId: "1e72753e-9dd2-46c8-a24f-36051899a7e1",
},
{
id: "cd7e43d3-aedb-48f9-8b5a-bdc22f4ffb81",
name: "高教教学科研能力提升",
recognizedHours: 4,
totalSeconds: 88850,
activitySetId: "47c25464-7e2c-431c-845f-7f63f9d14ac4",
},
];
const STORAGE_KEY = "smartedu-2026-training-settings";
const DEFAULT_SETTINGS = {
enabled: false,
muted: true,
playbackRate: 1,
autoNextCourse: true,
lastEndedResourceText: "",
};
let settings = loadSettings();
let lastActionAt = 0;
let lastStatus = "等待页面加载";
let boundVideo = null;
let tickTimer = null;
let waitingForProgressSave = false;
let tickRunning = false;
const catalogCache = new Map();
function loadSettings() {
try {
return {
...DEFAULT_SETTINGS,
...JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}"),
};
} catch (error) {
console.warn("[研修助手] 配置读取失败,已使用默认配置。", error);
return { ...DEFAULT_SETTINGS };
}
}
function saveSettings(patch = {}) {
settings = { ...settings, ...patch };
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
renderPanel();
}
function getCoursePlan() {
return COURSES;
}
function getCurrentCourseId() {
return new URL(location.href).searchParams.get("courseId");
}
function getCurrentCourse() {
const id = getCurrentCourseId();
return getCoursePlan().find((course) => course.id === id) || null;
}
function getCourseUrl(course) {
const url = new URL(COURSE_BASE_URL);
url.searchParams.set("courseId", course.id);
Object.entries(COURSE_CONTEXT).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
return url.href;
}
function getRequiredProgress(course) {
if (!course) return 100;
return Math.ceil((course.recognizedHours * 3600 * 100) / course.totalSeconds);
}
function readCourseProgress() {
const progressText = document.querySelector(
'[class*="CourseIndex-module_progress-text_"]'
);
const value = Number.parseFloat(progressText?.textContent || "");
if (Number.isFinite(value)) return value;
const courseButton = Array.from(document.querySelectorAll("a")).find((item) =>
["开始学习", "继续学习", "复习回顾"].includes(item.textContent.trim())
);
return courseButton?.textContent.trim() === "复习回顾" ? 100 : null;
}
async function loadCourseResources(course) {
if (!course?.activitySetId) return [];
if (catalogCache.has(course.id)) return catalogCache.get(course.id);
const url = `https://s-file-2.ykt.cbern.com.cn/teach/s_course/v2/activity_sets/${course.activitySetId}/fulls.json`;
try {
const response = await fetch(url, { credentials: "omit" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
const resources = [];
const walk = (nodes = []) => {
nodes.forEach((node) => {
const activity = node?.relations?.activity;
if (activity) {
if (Array.isArray(activity.activity_resources)) {
activity.activity_resources.forEach((resource) => {
if (resource.resource_type !== "video") return;
resources.push({
id: resource.resource_id,
title:
resource.video_extend?.title ||
node.node_name ||
"",
seconds: Number(
resource.study_time ||
resource.video_extend?.duration ||
activity.study_time ||
0
),
});
});
} else if (activity.type === "x_video") {
resources.push({
id: activity.resource_id || activity.id,
title: activity.name || node.node_name || "",
seconds: Number(activity.study_time || 0),
});
}
}
if (Array.isArray(node.child_nodes)) walk(node.child_nodes);
});
};
walk(data.nodes);
catalogCache.set(course.id, resources);
return resources;
} catch (error) {
console.warn("[研修助手] 课程目录时长读取失败。", error);
return [];
}
}
async function readCompletedSeconds(course) {
const resources = await loadCourseResources(course);
if (!resources.length) return null;
const completedItems = getVideoResources().filter(isFinishedResource);
const countedIds = new Set();
let seconds = 0;
completedItems.forEach((item) => {
const itemText = getResourceText(item);
const resource = resources.find(
(entry) =>
!countedIds.has(entry.id) &&
entry.title &&
(itemText.includes(entry.title) || entry.title.includes(itemText))
);
if (!resource || countedIds.has(resource.id)) return;
countedIds.add(resource.id);
seconds += resource.seconds;
});
return seconds;
}
async function hasReachedRequiredHours() {
const course = getCurrentCourse();
const progress = readCourseProgress();
if (!course) return false;
const required = getRequiredProgress(course);
if (progress !== null && progress >= required) {
setStatus(
`课程进度 ${progress}%,已达到本课程约 ${course.recognizedHours} 学时要求`
);
return true;
}
const completedSeconds = await readCompletedSeconds(course);
const requiredSeconds = course.recognizedHours * 3600;
if (completedSeconds === null || completedSeconds < requiredSeconds) return false;
setStatus(
`已完成视频约 ${(completedSeconds / 3600).toFixed(2)} 小时,达到本课程 ${course.recognizedHours} 学时要求`
);
return true;
}
function isTrainingHome() {
return location.pathname.includes(`/jiaoshi/training/${TRAINING_ID}`);
}
function isCoursePage() {
return /\/(jiaoshi|teacherTraining)\/course(Index|Detail)/.test(location.pathname);
}
function isOnCooldown(ms = 2500) {
return Date.now() - lastActionAt < ms;
}
function markAction() {
lastActionAt = Date.now();
}
function setStatus(text) {
if (lastStatus === text) return;
lastStatus = text;
console.log(`[研修助手] ${text}`);
renderPanel();
}
function ensurePanel() {
if (document.getElementById("smartedu-2026-helper")) return;
const panel = document.createElement("section");
panel.id = "smartedu-2026-helper";
panel.innerHTML = `
2026 研修助手
`;
const style = document.createElement("style");
style.textContent = `
#smartedu-2026-helper {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 2147483647;
width: 260px;
box-sizing: border-box;
padding: 14px;
border: 1px solid rgba(30, 98, 236, .28);
border-radius: 12px;
background: rgba(255, 255, 255, .97);
color: #1f2329;
box-shadow: 0 8px 30px rgba(0, 0, 0, .18);
font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI",
"Microsoft YaHei", sans-serif;
}
#smartedu-2026-helper .s26-title {
margin-bottom: 10px;
color: #1e62ec;
font-size: 16px;
font-weight: 700;
}
#smartedu-2026-helper .s26-row,
#smartedu-2026-helper .s26-check {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: 7px 0;
}
#smartedu-2026-helper select {
min-width: 112px;
padding: 4px 6px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background: #fff;
}
#smartedu-2026-helper .s26-check {
justify-content: flex-start;
}
#smartedu-2026-helper .s26-status {
min-height: 40px;
margin: 10px 0;
padding: 8px;
border-radius: 7px;
background: #f4f7ff;
color: #4e5969;
word-break: break-word;
}
#smartedu-2026-helper .s26-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
#smartedu-2026-helper button {
padding: 7px 8px;
border: 0;
border-radius: 7px;
background: #1e62ec;
color: #fff;
cursor: pointer;
}
#smartedu-2026-helper button[data-action="next"] {
background: #5b6b83;
}
`;
document.documentElement.append(style, panel);
panel.querySelector('[data-action="rate"]').addEventListener("change", (event) => {
saveSettings({ playbackRate: Number(event.target.value) });
configureVideo(document.querySelector("video"));
});
panel.querySelector('[data-action="muted"]').addEventListener("change", (event) => {
saveSettings({ muted: event.target.checked });
configureVideo(document.querySelector("video"));
});
panel.querySelector('[data-action="auto-next"]').addEventListener("change", (event) => {
saveSettings({ autoNextCourse: event.target.checked });
});
panel.querySelector('[data-action="toggle"]').addEventListener("click", () => {
const enabled = !settings.enabled;
saveSettings({ enabled });
setStatus(enabled ? "已启动,正在寻找未完成视频" : "已暂停");
if (enabled) runTick(true);
});
panel.querySelector('[data-action="next"]').addEventListener("click", () => {
markAction();
if (!openNextUnfinishedVideo({ skipActive: true })) {
moveToNextCourse();
}
});
}
function renderPanel() {
const panel = document.getElementById("smartedu-2026-helper");
if (!panel) return;
panel.querySelector('[data-action="rate"]').value = String(settings.playbackRate);
panel.querySelector('[data-action="muted"]').checked = settings.muted;
panel.querySelector('[data-action="auto-next"]').checked = settings.autoNextCourse;
panel.querySelector('[data-action="toggle"]').textContent = settings.enabled ? "暂停" : "开始";
const course = getCurrentCourse();
const prefix = course ? `${course.name}:` : "";
panel.querySelector('[data-role="status"]').textContent = `${prefix}${lastStatus}`;
}
function expandCatalogs() {
document
.querySelectorAll('.fish-collapse-header[aria-expanded="false"]')
.forEach((header) => header.click());
}
function isVideoResource(item) {
const image = item.querySelector("img");
if (!image) return true;
return /video/i.test(image.src);
}
function isFinishedResource(item) {
return Boolean(
item.querySelector(
'.icon_checkbox_fill[title="已学完"], .iconfont.icon_checkbox_fill'
)
);
}
function getResourceText(item) {
return (item?.textContent || "").replace(/\s+/g, " ").trim();
}
function getVideoResources() {
expandCatalogs();
return Array.from(
document.querySelectorAll(".resource-item.resource-item-train")
).filter(isVideoResource);
}
function openNextUnfinishedVideo({ skipActive = false } = {}) {
const items = getVideoResources();
if (!items.length) return false;
waitingForProgressSave = false;
const activeIndex = items.findIndex((item) =>
item.classList.contains("resource-item-active")
);
const lastEndedIndex = settings.lastEndedResourceText
? items.findIndex(
(item) => getResourceText(item) === settings.lastEndedResourceText
)
: -1;
const referenceIndex = activeIndex >= 0 ? activeIndex : lastEndedIndex;
const isCandidate = (item) =>
!isFinishedResource(item) &&
getResourceText(item) !== settings.lastEndedResourceText &&
(!skipActive || !item.classList.contains("resource-item-active"));
// 页面重新打开时,优先恢复平台标记为“进行中”的资源;没有才找首个未开始资源。
let target = !skipActive
? items.find(
(item) =>
isCandidate(item) &&
item.querySelector(
'.icon_processing_fill[title="进行中"], .iconfont.icon_processing_fill'
)
)
: null;
if (!target && referenceIndex >= 0) {
target =
items.slice(referenceIndex + 1).find(isCandidate) ||
items.slice(0, referenceIndex).find(isCandidate);
} else if (!target) {
target = items.find(isCandidate);
}
if (!target) {
const endedItem = lastEndedIndex >= 0 ? items[lastEndedIndex] : null;
if (endedItem && !isFinishedResource(endedItem)) {
waitingForProgressSave = true;
setStatus("等待平台把刚播放的视频更新为“已学完”");
}
return false;
}
const title = getResourceText(target);
markAction();
target.click();
setStatus(`正在打开:${title || "下一视频"}`);
return true;
}
function closeKnownPlaybackTip() {
const buttons = Array.from(document.querySelectorAll("button"));
const button = buttons.find((item) =>
["知道了", "我知道了"].includes(item.textContent.trim())
);
if (button) button.click();
}
function configureVideo(video) {
if (!video) return false;
video.muted = Boolean(settings.muted);
video.playbackRate = Number(settings.playbackRate) || 1;
if (boundVideo !== video) {
boundVideo = video;
video.addEventListener("ended", onVideoEnded);
video.addEventListener("play", () => setStatus("视频正在播放"));
video.addEventListener("waiting", () => setStatus("视频缓冲中"));
video.addEventListener("error", () => setStatus("播放器报错,请刷新页面后重试"));
}
if (video.paused && !video.ended && settings.enabled) {
video.play().catch(() => {
setStatus("浏览器阻止了自动播放,请手动点一次播放");
});
}
return true;
}
function onVideoEnded() {
setStatus("本视频播放完成,等待平台保存进度");
const activeItem = document.querySelector(
".resource-item.resource-item-train.resource-item-active"
);
const activeText = getResourceText(activeItem);
if (activeText) saveSettings({ lastEndedResourceText: activeText });
window.setTimeout(async () => {
if (!settings.enabled) return;
if (await hasReachedRequiredHours()) {
moveToNextCourse();
return;
}
if (openNextUnfinishedVideo({ skipActive: true })) return;
if (waitingForProgressSave) return;
const course = getCurrentCourse();
if (course) location.href = getCourseUrl(course);
}, 5000);
}
function moveToNextCourse() {
if (!settings.autoNextCourse) {
setStatus("当前课程暂无未完成视频");
return false;
}
const plan = getCoursePlan();
const currentId = getCurrentCourseId();
const index = plan.findIndex((course) => course.id === currentId);
if (index >= 0 && index + 1 < plan.length) {
const nextCourse = plan[index + 1];
setStatus(`切换到:${nextCourse.name}`);
markAction();
location.href = getCourseUrl(nextCourse);
return true;
}
saveSettings({ enabled: false });
setStatus("课程列表已处理完,请回专题页核对认定学时");
return false;
}
function startPlan() {
const plan = getCoursePlan();
const current = getCurrentCourse();
const target = current || plan[0];
markAction();
location.href = getCourseUrl(target);
}
async function runTick(force = false) {
if (tickRunning) return;
tickRunning = true;
try {
ensurePanel();
renderPanel();
if (!settings.enabled) return;
if (!force && isOnCooldown()) return;
closeKnownPlaybackTip();
if (isTrainingHome()) {
setStatus("正在检查第一门尚未达到认定学时的课程");
startPlan();
return;
}
if (!isCoursePage()) {
setStatus("请打开2026年暑期教师研修专题或课程页");
return;
}
// 必须在调用 video.play() 之前判断,否则播放页会提前 return,永远不会切课。
if (await hasReachedRequiredHours()) {
const currentVideo = document.querySelector("video");
if (currentVideo && !currentVideo.paused) currentVideo.pause();
moveToNextCourse();
return;
}
const video = document.querySelector("video");
if (configureVideo(video)) return;
const loginPrompt = Array.from(document.querySelectorAll("button, a")).some(
(item) => item.textContent.trim() === "登录"
);
if (
loginPrompt &&
!document.querySelector(".resource-item.resource-item-train")
) {
setStatus("请先登录并完成专题报名");
return;
}
if (openNextUnfinishedVideo()) return;
if (waitingForProgressSave) return;
const courseButton = Array.from(document.querySelectorAll("a")).find(
(item) => ["开始学习", "继续学习"].includes(item.textContent.trim())
);
if (courseButton) {
markAction();
courseButton.click();
setStatus("正在进入课程播放器");
return;
}
setStatus("等待课程目录或播放器加载");
} finally {
tickRunning = false;
}
}
ensurePanel();
renderPanel();
tickTimer = window.setInterval(runTick, 3000);
runTick(true);
// 平台或浏览器因页面失焦而暂停时,不尝试规避检测;回到前台后自动重新尝试播放。
document.addEventListener("visibilitychange", () => {
if (!document.hidden && settings.enabled) {
setStatus("页面已恢复前台,正在继续播放");
runTick(true);
}
});
window.addEventListener("focus", () => {
if (settings.enabled) runTick(true);
});
window.addEventListener("beforeunload", () => {
if (tickTimer) window.clearInterval(tickTimer);
});
})();