From 0cf366a5ef082496a0108902fd58dfa874e9eb98 Mon Sep 17 00:00:00 2001 From: enncy <877526278@qq.com> Date: Sun, 19 Mar 2023 19:11:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(script):=20=E6=96=B0=E5=A2=9E=E3=80=90?= =?UTF-8?q?=E8=81=8C=E6=95=99=E4=BA=91=E3=80=91=E5=92=8C=E3=80=90=E6=99=BA?= =?UTF-8?q?=E6=85=A7=E8=81=8C=E6=95=99=E3=80=91=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/scripts/src/projects/icve.ts | 151 ++++++++++++ packages/scripts/src/projects/zjy.ts | 343 ++++++++++++++++++++++++++ packages/scripts/src/utils/index.ts | 66 +++++ 3 files changed, 560 insertions(+) create mode 100644 packages/scripts/src/projects/icve.ts create mode 100644 packages/scripts/src/projects/zjy.ts diff --git a/packages/scripts/src/projects/icve.ts b/packages/scripts/src/projects/icve.ts new file mode 100644 index 00000000..8bc7ce31 --- /dev/null +++ b/packages/scripts/src/projects/icve.ts @@ -0,0 +1,151 @@ +import { $el, Project, Script, $, $script, $$el, $creator, $model, $message } from '@ocsjs/core'; +import { volume } from '../utils/configs'; +import { createRangeTooltip, playMedia } from '../utils'; + +const state = { + study: { + currentMedia: undefined as HTMLMediaElement | undefined + } +}; + +export const ICVEProject = Project.create({ + name: '智慧职教', + domains: ['icve.com.cn', 'course.icve.com.cn'], + studyProject: true, + scripts: { + study: new Script({ + name: '🧑‍💻 课程学习', + namespace: 'icve.study.main', + url: [['课程学习页面', 'course.icve.com.cn/learnspace/learn/learn/templateeight/index.action']], + configs: { + notes: { + defaultValue: $creator.notes(['请手动点击任意章节以触发自动学习脚本']).outerHTML + }, + volume, + playbackRate: { + label: '视频倍速', + attrs: { + type: 'range', + step: 1, + min: 1, + max: 16 + }, + defaultValue: 1, + onload() { + createRangeTooltip(this, '1', (val) => `${val}x`); + } + }, + showScrollBar: { + label: '显示右侧滚动条', + attrs: { type: 'checkbox' }, + defaultValue: true + }, + expandAll: { + label: '展开所有章节', + attrs: { type: 'checkbox' }, + defaultValue: true + } + }, + async oncomplete() { + $script.pin(this); + + await $.sleep(3000); + + this.onConfigChange('volume', (v) => state.study.currentMedia && (state.study.currentMedia.volume = v)); + this.onConfigChange( + 'playbackRate', + (r) => state.study.currentMedia && (state.study.currentMedia.playbackRate = r) + ); + + const mainContentWin = $el('#mainContent')?.contentWindow as Window & { [x: string]: any }; + + if (mainContentWin) { + const _openLearnResItem: Function = mainContentWin.openLearnResItem; + mainContentWin.openLearnResItem = async (...args: any[]) => { + // 调用原函数 + _openLearnResItem.apply(mainContentWin, args); + await $.sleep(3000); + await study(); + }; + } + + if (this.cfg.showScrollBar) { + const bar = $el('.dumascroll_area', mainContentWin.document); + bar && (bar.style.overflow = 'auto'); + } + + if (this.cfg.expandAll) { + $$el('.s_sectionlist,.s_sectionwrap', mainContentWin.document).forEach((el) => (el.style.display = 'block')); + } + + // 任务点 + const jobs = $$el('.item_done_icon:not(.done_icon_show)', mainContentWin.document); + + console.log(jobs); + + /** 学习 */ + const study = async () => { + const iframe = $el('iframe', mainContentWin.document); + const win = iframe?.contentWindow; + if (win) { + const doc = win.document; + if (iframe.src.includes('content_video.action')) { + // 视频 + const video = $el('video', doc); + state.study.currentMedia = video; + + if (video) { + if (video.ended) { + video.currentTime = 0; + } + + video.playbackRate = this.cfg.playbackRate; + video.volume = this.cfg.volume; + + await new Promise((resolve, reject) => { + try { + video.addEventListener('ended', async () => { + await $.sleep(3000); + resolve(); + }); + video.addEventListener('pause', async () => { + if (!video.ended) { + await $.sleep(1000); + playMedia(() => video.play()); + } + }); + // 开始播放 + playMedia(() => video.play()); + } catch (err) { + reject(err); + } + }); + } else { + $message('error', { content: '未检测到视频,请刷新页面重试。' }); + } + } else if (iframe.src.includes('content_doc.action')) { + // 文档只需点击就算完成,等待5秒下一个 + await $.sleep(5000); + } + } else { + // 如果为 null 证明跨域 + } + + // 递归调用直到完成为止 + if (jobs.length) { + const job = jobs.shift(); + // 如果不是当前所处的任务点,则点击,否则可直接开始学习 + if (job) { + // 这里不要调用 study() !!!,是通过上面回调进行调用 study,这里触发 openLearnResItem 即可 + job.click(); + } + } else { + $model('alert', { + content: '全部任务已完成' + }); + } + }; + } + }) + } +}); diff --git a/packages/scripts/src/projects/zjy.ts b/packages/scripts/src/projects/zjy.ts new file mode 100644 index 00000000..fbfbd44b --- /dev/null +++ b/packages/scripts/src/projects/zjy.ts @@ -0,0 +1,343 @@ +import { + $, + $$el, + $creator, + $el, + $gm, + $message, + $model, + $script, + $store, + Project, + Script, + domSearch +} from '@ocsjs/core'; +import { $console } from './background'; +import { playMedia } from '../utils'; +import { volume } from '../utils/configs'; + +const state = { + loading: false, + finish: false, + study: { + currentMedia: undefined as HTMLMediaElement | undefined + } +}; + +/** + * 职教云网课 + * + * 因为存在子 iframe 并且 ppt 跨域的情况 + * 所以采用新建小窗口的形式,通过子 window 以及 opener 的形式进行互相回调调用 + * 所以核心逻辑代码可能会比较绕。 + * + * 为什么不在学习页面写脚本,而是 课程学习 和 学习页面 两个脚本进行交互运行? + * 因为学习页面无法获取学习进度,这样可能导致已学课程重复学习。 + * + */ +export const ZJYProject = Project.create({ + name: '职教云', + domains: ['icve.com.cn', 'zjy2.icve.com.cn'], + studyProject: true, + scripts: { + studyDispatcher: new Script({ + name: '🧑‍💻 课程学习', + url: [['课程页面', 'zjy2.icve.com.cn/study/process/process']], + namespace: 'zjy.study.dispatcher', + configs: { + notes: { + defaultValue: '请点击任意章节进行学习。' + } + } + }), + /** 因为阅读脚本跨域,所以这里通过监听页面数据进行回调反馈,通过修改数据,触发学习页面的回调。 */ + read: new Script({ + name: '阅读脚本', + hideInPanel: true, + url: [['ppt页面', 'file.icve.com.cn']], + async oncomplete() { + await $.sleep(10 * 1000); + + console.log('reading', true); + $store.setTab('reading', true); + const fixTime = $gm.unsafeWindow._fixTime || 10; + + while (true) { + const { gc, Presentation } = $gm.unsafeWindow; + + const { TotalSlides } = Presentation.GetContentDetails(); + if (gc < TotalSlides) { + console.log(gc, TotalSlides); + await $.sleep(1000); + // @ts-ignore + Presentation.Next(); + } else { + break; + } + } + $console.info(`PPT播放完成 ${fixTime * 2} 秒后将自动切换下一个任务。`); + await $.sleep(1000 * fixTime * 2); + $store.setTab('reading', false); + } + }), + study: new Script({ + name: '🧑‍💻 学习脚本', + url: [['学习页面', 'zjy2.icve.com.cn/common/directory/directory.html']], + namespace: 'zjy.study.main', + configs: { + notes: { + defaultValue: $creator.notes([ + '如果脚本卡死或者您不想学习,可以点击任意章节继续进行学习。', + '提示:职教云无法使用倍速。' + ]).outerHTML + }, + volume: volume + }, + async onstart() { + $script.pin(this); + + this.onConfigChange('volume', (volume) => { + if (state.study.currentMedia) { + state.study.currentMedia.volume = volume; + } + }); + }, + async oncomplete() { + await $.sleep(1000); + // 展开目录 + const sildeDirectory = $el('.sildeDirectory'); + sildeDirectory?.click(); + await $.sleep(1000); + sildeDirectory?.click(); + + /** 展开下一个列表 */ + const getNextTopicListOpener = () => + $$el('.topicList') + .find((c) => $el('.topicCellContainer', c)?.children.length === 0) + ?.querySelector('a'); + + /** 展开下一个模块 */ + const getNextModuleOpener = () => + $$el('.moduleList').find((c) => $el('.moduleTopicContainer', c)?.children.length === 0); + + const getActiveNode = () => $el('li[data-cellid].active'); + + const getNextNode = () => { + const active = getActiveNode(); + if (active) { + const next = $el(`li[data-upcellid="${active.dataset.cellid}"]`); + if (next) { + return next; + } + } + }; + + // 下一章 + const next = async () => { + let el; + let tlo; + let mo; + while (!el) { + el = getNextNode(); + if (!el) { + tlo = getNextTopicListOpener(); + mo = getNextModuleOpener(); + + /** + * 如果找不到目标节点,说明没有展开 + * 这里递归展开直到找到为止 + */ + + if (tlo) { + tlo.click(); + await $.sleep(5000); + } else if (mo) { + mo.click(); + await $.sleep(5000); + } else { + // 如果都没有,则说明已经全部展开 + break; + } + } + + await $.sleep(3000); + } + + el && el.click(); + + return !!el; + }; + + const studyLoop = async () => { + const studyNow = $el('#studyNow'); + if (studyNow) { + studyNow.click(); + } + // 等待点击 + await $.sleep(3000); + try { + const active = getActiveNode(); + if (active) { + await start(active.textContent || '未知任务', document); + + if (await next()) { + await studyLoop(); + } else { + $model('alert', { + content: '检测不到下一章任务点,请检查是否已经全部完成。' + }); + } + } + } catch (error) { + $console.error('未知错误:', error); + } + }; + + await studyLoop(); + } + }) + } +}); + +/** + * 创建弹出窗口 + * @param url 地址 + * @param winName 窗口名 + * @param width 宽 + * @param height 高 + * @param scrollbars 是否有滚动条 + * @param resizable 是否可调整大小 + */ +export function createPopupWindow( + url: string, + name: string, + opts: { + width: number; + height: number; + scrollbars: boolean; + resizable: boolean; + } +) { + const { width, height, scrollbars, resizable } = opts; + const LeftPosition = screen.width ? (screen.width - width) / 2 : 0; + const TopPosition = screen.height ? (screen.height - height) / 2 : 0; + const settings = + 'height=' + + height + + ',width=' + + width + + ',top=' + + TopPosition + + ',left=' + + LeftPosition + + ',scrollbars=' + + (scrollbars ? 'yes' : 'no') + + ',resizable=' + + (resizable ? 'yes' : 'no'); + + return window.open(url, name, settings); +} + +/** + * 永久固定显示视频进度 + */ +export function fixedVideoProgress(doc: Document) { + const bar = $el('.jw-controlbar', doc); + if (state.study.currentMedia && bar) { + bar.style.display = 'block'; + bar.style.visibility = 'visible'; + bar.style.opacity = '1'; + } +} + +function start(name: string, doc: Document) { + return new Promise((resolve, reject) => { + (async () => { + const fixTime = $gm.unsafeWindow._fixTime || 10; + const { ppt, video, iframe, link, docPlay, nocaptcha } = domSearch( + { + // ppt + ppt: '.page-bar', + // ppt + iframe: 'iframe', + // 视频 + video: 'video', + // 链接 + link: '#externalLinkDiv', + // 图文/图片 + docPlay: '#docPlay', + // 验证码 + nocaptcha: '#waf_nc_block,#nocaptcha' + }, + doc + ); + + console.log({ doc, ppt, video, iframe, link, docPlay, nocaptcha }); + + if (nocaptcha && nocaptcha.style.display !== 'none') { + $message('warn', { content: '请手动滑动验证码。' }); + } else if (video) { + // 如果 iframe 不存在则表示只有视频任务,否则表示PPT任务正在运行 + $console.info('开始播放:', name); + const _video = video as HTMLVideoElement; + const jp = $gm.unsafeWindow.jwplayer($gm.unsafeWindow.$('.video_container').attr('id')); + + video.onended = async () => { + $console.info('视频结束:', name); + await $.sleep(3000); + resolve(); + }; + // 固定进度 + fixedVideoProgress(doc); + // 设置音量 + _video.volume = 0; + + if (_video.paused) { + playMedia(() => jp.play()); + } + } else if (iframe && (iframe as HTMLIFrameElement).src.startsWith('https://file.icve.com.cn')) { + // 监听阅读任务执行完毕 + const id = + (await $store.addTabChangeListener('reading', (reading) => { + if (reading === false) { + $store.removeTabChangeListener('reading', id); + resolve(); + } + })) || 0; + } else if (ppt) { + $console.info('开始播放: ', name); + const { pageCount, pageCurrentCount, pageNext } = domSearch({ + pageCount: '.MPreview-pageCount', + pageNext: '.MPreview-pageNext', + pageCurrentCount: '.MPreview-pageInput' + }); + if (pageCurrentCount && pageCount && pageNext) { + // @ts-ignore + let count = parseInt(pageCurrentCount.value); + const total = parseInt(pageCount.innerText.replace('/', '').trim() || '0'); + + while (count <= total) { + // @ts-ignore + pageNext.click(); + await $.sleep(1000); + count++; + } + $console.info(`${name} 播放完成, ${fixTime * 2} 秒后将自动切换下一个任务。`); + await $.sleep(1000 * fixTime * 2); + resolve(); + } else { + $message('error', { content: '未找到PPT进度,请刷新重试或者跳过此任务。' }); + } + } else if ((link && link.style.display !== 'none') || docPlay) { + $console.info(`${name} 查看完成,${fixTime}秒后下一个任务`); + // 等待学习任务进行记录再下一章 + await $.sleep(1000 * fixTime + 1); + resolve(); + } else { + $console.error(`${name} : 未知的课件类型,请联系作者进行反馈,${fixTime}秒后下一个任务。`); + await $.sleep(1000 * fixTime + 1); + resolve(); + } + })(); + }); +} diff --git a/packages/scripts/src/utils/index.ts b/packages/scripts/src/utils/index.ts index 6ad850f1..a651d98a 100644 --- a/packages/scripts/src/utils/index.ts +++ b/packages/scripts/src/utils/index.ts @@ -1,3 +1,47 @@ +import { $creator, $message, $model, AnswererWrapper, WorkUploadType, el } from '@ocsjs/core'; + +export interface CommonWorkOptions { + period: number; + thread: number; + upload: WorkUploadType; + uncheckAllChoice: boolean; + answererWrappers: AnswererWrapper[]; + stopSecondWhenFinish: number; +} + +/** 创建答题预处理信息 */ +export function workPreCheckMessage( + options: CommonWorkOptions & { + onrun: (opts: CommonWorkOptions) => void; + ondone?: (opts: CommonWorkOptions) => void; + } +) { + const { onrun, ondone, ...opts } = options; + + if (opts.answererWrappers.length === 0) { + $model('alert', { content: '检测到题库配置为空,无法自动答题,请前往全局设置页面进行配置。' }); + ondone?.(opts); + } else { + $message('info', { + duration: 5, + content: el('span', [ + '5秒后自动答题。并切换到“通用-搜索结果”。', + $creator.preventText({ + name: '点击取消此次答题', + delay: 5, + ondefault: (span) => { + onrun(opts); + }, + onprevent(span) { + $message('warn', { content: '已关闭此次的自动答题。' }); + ondone?.(opts); + } + }) + ]) + }); + } +} + /** * 创造范围选择器的提示 */ @@ -11,3 +55,25 @@ export function createRangeTooltip( }); input.setAttribute('data-title', transform(input.value || input.getAttribute('value') || defaultValue)); } + +export function playMedia(playFunction: () => Promise) { + return new Promise((resolve, reject) => { + // 有些网课会改变 media.play 方法,所以可能不是一个 promise + const playRes = playFunction(); + console.log('playRes', playFunction, playRes); + + playRes?.then(resolve).catch((err) => { + console.log('play error', err); + + if (String(err).includes(`failed because the user didn't interact with the document first`)) { + $model('alert', { + content: + '由于浏览器保护限制,如果要播放带有音量的视频,您必须先点击页面上的任意位置才能进行视频的播放,如果想自动播放,必须先在设置页面静音,然后重新运行脚本。', + onClose: () => playFunction().then(resolve).catch(reject) + }); + } else { + reject(err); + } + }); + }); +}