// ==UserScript== // @name 山东教师研修2026-播放与章节助手 // @namespace local.qlteacher.helper // @version 3.0.0 // @description 自动正常播放、完成后切换视频章节;遇到题目提醒人工处理。 // @match https://yxjc.qlteacher.com/project/gz2026/* // @match https://yxjc.qlteacher.com/learning/gz2026/* // @match https://player.qlteacher.com/learning/* // @grant GM_getValue // @grant GM_setValue // @run-at document-end // @noframes // ==/UserScript== (() => { 'use strict'; const LIST = 'https://yxjc.qlteacher.com/project/gz2026/lesson/learn'; const KEY = 'qlteacher2026.worker.v3'; const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; const isPlayer = location.hostname === 'player.qlteacher.com'; const isList = location.pathname === '/project/gz2026/lesson/learn'; let running = false, busy = false, nextAction = 0, lastPlay = 0; let lastUrl = location.href, completionSince = 0, pending = false; const finished = new Set(); const panel = document.createElement('div'); panel.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;background:#fff;color:#222;border:1px solid #aaa;border-radius:10px;padding:12px;max-width:320px;font:14px/1.6 sans-serif;box-shadow:0 2px 12px #0003'; const status = document.createElement('div'); const button = document.createElement('button'); button.textContent = '启动助手'; panel.append(status, button); document.body.append(panel); const say = text => { status.textContent = text; }; const text = e => (e?.innerText || '').replace(/\s+/g, '').trim(); const visible = e => !!e && e.getClientRects().length > 0 && getComputedStyle(e).visibility !== 'hidden'; const lease = () => GM_getValue(KEY, null); const active = value => value && Date.now() - value.time < 120000; function release() { if (lease()?.id === id) GM_setValue(KEY, null); } function stop(message) { running = false; release(); button.textContent = '启动助手'; say(message); } function start() { const owner = lease(); if (active(owner) && owner.id !== id) { say('另一个标签页正在处理课程,请先在那里停止助手。'); return; } running = true; GM_setValue(KEY, { id, time: Date.now(), pending: false }); button.textContent = '停止助手'; say('已启动'); } button.onclick = () => running ? stop('已停止助手;视频可自行暂停。') : start(); say('待启动;请先禁用旧脚本,避免两个脚本同时操作。'); // 列表点击会打开新页。仅允许这个新播放器接管一次,避免重复开课。 if (isPlayer || (!isList && location.hostname === 'yxjc.qlteacher.com')) { const owner = lease(); if (active(owner) && owner.pending) { running = true; pending = !isPlayer; GM_setValue(KEY, { id, time: Date.now(), pending: !isPlayer }); button.textContent = '停止助手'; } } function questionVisible() { return [...document.querySelectorAll('input[type="radio"],input[type="checkbox"],.ant-radio,.ant-checkbox')] .some(e => visible(e) || visible(e.closest('label'))) || [...document.querySelectorAll('.segmented-text-ellipsis')].some(e => visible(e) && text(e) === '测试题'); } function click(el) { nextAction = Date.now() + 6000; el.click(); } async function tick() { if (!running || busy) return; busy = true; try { const owner = lease(); if (active(owner) && owner.id !== id) { running = false; button.textContent = '启动助手'; say('已交由新课程标签页处理。'); return; } GM_setValue(KEY, { id, time: Date.now(), pending }); if (location.href !== lastUrl) { lastUrl = location.href; completionSince = 0; nextAction = Date.now() + 4000; } if (Date.now() < nextAction) return; if (isList) { if (pending) { stop('新课程未接管,请检查是否被拦截弹窗,或在播放器页启动助手。'); return; } const buttons = [...document.querySelectorAll('button')].filter(e => visible(e) && !e.disabled); const target = buttons.find(e => text(e) === '继续学习') || buttons.find(e => text(e) === '开始学习'); if (!target) { say('未找到待学习按钮,请核对课程状态。'); return; } pending = true; GM_setValue(KEY, { id, time: Date.now(), pending: true }); nextAction = Date.now() + 60000; target.click(); say('正在打开课程,等待播放器接管……'); return; } if (!isPlayer) { say('等待平台自动跳转到播放器……'); return; } if (questionVisible()) { document.querySelector('video')?.pause(); say('发现题目,请自行作答;完成后助手会继续。'); return; } if ([...document.querySelectorAll('.ant-modal,.ant-modal-confirm,.ant-drawer-open')].some(visible)) { say('发现弹窗,请先处理弹窗。'); return; } const complete = [...document.querySelectorAll('.count-down')].some(e => visible(e) && /^已完成(?:学习)?$/.test(text(e))); if (complete) { if (!completionSince) completionSince = Date.now(); if (Date.now() - completionSince < 3000) return; const rows = [...document.querySelectorAll('li.ant-list-item')].filter(e => e.querySelector('[aria-label="play-circle"]') && e.querySelector('ellipsis[title]')); const title = document.querySelector('h1')?.innerText.trim(); const index = rows.findIndex(e => e.querySelector('ellipsis')?.getAttribute('title') === title); if (index < 0) { stop('本节已完成,但无法定位当前章节,请人工确认。'); return; } finished.add(title); const next = rows.slice(index + 1).find(e => !finished.has(e.querySelector('ellipsis').getAttribute('title'))); if (next) { say('本节已完成,切换下一视频章节……'); completionSince = 0; click(next); } else { stop('本课程视频章节已完成,请核对资料或测试要求,再返回列表启动下一课。'); const back = document.createElement('a'); back.href = LIST; back.textContent = '返回课程列表'; panel.append(back); } return; } completionSince = 0; const video = document.querySelector('video'); if (!video) { say('等待视频加载;非视频内容请人工处理。'); return; } if (video.error) { say('视频加载失败,请检查网络。'); return; } if (video.ended) { say('视频已结束,等待平台确认完成。'); return; } if (video.paused && Date.now() - lastPlay > 8000) { lastPlay = Date.now(); video.muted = true; try { await video.play(); } catch { say('浏览器阻止自动播放,请手动点击一次播放。'); return; } } say(video.paused ? '等待播放……' : '正常速度静音播放中;遇题目会提醒。'); } catch (error) { say(`处理失败:${error.message}`); console.error('[研修助手]', error); nextAction = Date.now() + 8000; } finally { busy = false; } } setInterval(tick, 1500); })();