您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
作业显示相关课程,office文档预览切换到 view.officeapps.live.com,重命名下载文件名
// ==UserScript== // @name 云邮教学空间助手 // @namespace http://tampermonkey.net/ // @version 0.20 // @description 作业显示相关课程,office文档预览切换到 view.officeapps.live.com,重命名下载文件名 // @author YouXam // @match https://ucloud.bupt.edu.cn/* // @match https://ucloud.bupt.edu.cn/uclass/course.html* // @match https://ucloud.bupt.edu.cn/uclass/* // @match https://ucloud.bupt.edu.cn/office/* // @icon https://ucloud.bupt.edu.cn/favicon.ico // @grant none // @license MIT // ==/UserScript== /* eslint-disable no-undef */ window.loadJs = function loadJs(src) { return new Promise((ret, rej) => { fetch(src) .then((res) => res.text()) .then((res) => { const s = document.createElement('script') s.language = 'javascript' s.type = 'text/javascript' s.text = res document.getElementsByTagName('HEAD')[0].appendChild(s) ret(0) }) .catch((e) => rej(e)) }) } function loadCss() { function addGlobalStyle(css) { const head = document.getElementsByTagName('head')[0] if (!head) { return } const style = document.createElement('style') style.type = 'text/css' style.innerHTML = css head.appendChild(style) } addGlobalStyle( '#nprogress{pointer-events:none}#nprogress .bar{background:#ffbe00;position:fixed;z-index:1031;top:0;left:0;width:100%;height:5px}#nprogress .peg,.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}#nprogress .peg{display:block;right:0;width:100px;height:100%;box-shadow:0 0 10px #ffbe00,0 0 5px #ffbe00;opacity:1;-webkit-transform:rotate(3deg) translate(0,-4px);-ms-transform:rotate(3deg) translate(0,-4px);transform:rotate(3deg) translate(0,-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:2px solid transparent;border-top-color:#ffbe00;border-left-color:#ffbe00;border-radius:50%;-webkit-animation:.4s linear infinite nprogress-spinner;animation:.4s linear infinite nprogress-spinner}.nprogress-custom-parent{overflow:hidden;position:relative}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}' ) } function getToken() { const cookieMap = new Map() document.cookie.split('; ').forEach((cookie) => { const [key, value] = cookie.split('=') cookieMap.set(key, value) }) const token = cookieMap.get('iClass-token') const userid = cookieMap.get('iClass-uuid') return [userid, token] } let sumBytes = 0, loadedBytes = 0, downloading = false async function downloadFile(url, filename) { downloading = true await jsp NProgress.configure({ trickle: false, speed: 0 }) try { const response = await fetch(url) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const contentLength = response.headers.get('content-length') if (!contentLength) { window.open(url) return throw new Error('Content-Length response header unavailable') } const total = parseInt(contentLength, 10) sumBytes += total const reader = response.body.getReader() const chunks = [] while (true) { const { done, value } = await reader.read() if (done) break if (!downloading) { NProgress.done() return } chunks.push(value) loadedBytes += value.length NProgress.set(loadedBytes / sumBytes) } NProgress.done() sumBytes -= total loadedBytes -= total const blob = new Blob(chunks) const downloadUrl = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = downloadUrl a.download = filename document.body.appendChild(a) a.click() window.URL.revokeObjectURL(downloadUrl) } catch (error) { console.error('Download failed:', error) } } async function searchTask(siteId, keyword, token) { const res = await fetch( 'https://apiucloud.bupt.edu.cn/ykt-site/work/student/list', { headers: { authorization: 'Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=', 'blade-auth': token, 'content-type': 'application/json;charset=UTF-8' }, body: JSON.stringify({ siteId, keyword, current: 1, size: 5 }), method: 'POST' } ) const json = await res.json() return json } async function searchCourse(userId, id, keyword, token) { const res = await fetch( 'https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999¤t=1&userId=' + userId + '&siteRoleCode=2', { headers: { authorization: 'Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=', 'blade-auth': token }, body: null, method: 'GET' } ) const json = await res.json() const list = json.data.records.map((x) => ({ id: x.id, name: x.siteName, teachers: x.teachers.map((y) => y.name).join(', ') })) async function searchWithLimit(list, id, keyword, token, limit = 5) { for (let i = 0; i < list.length; i += limit) { const batch = list.slice(i, i + limit) const jobs = batch.map((x) => searchTask(x.id, keyword, token)) const ress = await Promise.all(jobs) for (let j = 0; j < ress.length; j++) { const res = ress[j] if (res.data.records.length > 0) { for (const item of res.data.records) { if (item.id == id) { return batch[j] } } } } } return null } return await searchWithLimit(list, id, keyword, token) } async function getTasks(siteId, token) { const res = await fetch( 'https://apiucloud.bupt.edu.cn/ykt-site/work/student/list', { headers: { authorization: 'Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=', 'blade-auth': token, 'content-type': 'application/json;charset=UTF-8' }, body: JSON.stringify({ siteId, current: 1, size: 9999 }), method: 'POST' } ) const json = await res.json() return json } async function searchCourses(nids) { const result = {} const ids = [] for (const id of nids) { const r = get(id) if (r) result[id] = r else ids.push(id) } if (ids.length == 0) return result const [userid, token] = getToken() const res = await fetch( 'https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999¤t=1&userId=' + userid + '&siteRoleCode=2', { headers: { authorization: 'Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=', 'blade-auth': token }, body: null, method: 'GET' } ) const json = await res.json() const list = json.data.records.map((x) => ({ id: x.id, name: x.siteName, teachers: x.teachers.map((y) => y.name).join(', ') })) const hashMap = new Map() let count = ids.length for (let i = 0; i < ids.length; i++) { hashMap.set(ids[i], i) } async function searchWithLimit(list, limit = 5) { for (let i = 0; i < list.length; i += limit) { const batch = list.slice(i, i + limit) const jobs = batch.map((x) => getTasks(x.id, token)) const ress = await Promise.all(jobs) for (let j = 0; j < ress.length; j++) { const res = ress[j] if (res.data.records.length > 0) { for (const item of res.data.records) { if (hashMap.has(item.id)) { result[item.id] = batch[j] set(item.id, batch[j]) if (--count == 0) { return result } } } } } } return result } return await searchWithLimit(list) } async function getUndoneList() { const [userid, token] = getToken() const res = await fetch( 'https://apiucloud.bupt.edu.cn/ykt-site/site/student/undone?userId=' + userid, { headers: { authorization: 'Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=', 'blade-auth': token }, method: 'GET' } ) const json = await res.json() return json } async function getDetail(id) { const [, token] = getToken() const res = await fetch( 'https://apiucloud.bupt.edu.cn/ykt-site/work/detail?assignmentId=' + id, { headers: { authorization: 'Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=', 'blade-auth': token }, body: null, method: 'GET' } ) const json = await res.json() return json } async function getSiteResource(id) { const [userid, token] = getToken() const res = await fetch( 'https://apiucloud.bupt.edu.cn/ykt-site/site-resource/tree/student?siteId=' + id + '&userId=' + userid, { headers: { authorization: 'Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=', 'blade-auth': token }, body: null, method: 'POST' } ) const json = await res.json() const result = [] function foreach(data) { console.log(data) data.forEach((x) => { x.attachmentVOs.forEach((y) => { if (y.type !== 2) result.push(y.resource) }) foreach(x.children) }) } foreach(json.data) return result } function $x(xpath, context = document) { const iterator = document.evaluate( xpath, context, null, XPathResult.ANY_TYPE, null ) const results = [] let item while ((item = iterator.iterateNext())) { results.push(item) } return results } function set(k, v) { const h = JSON.parse(localStorage.getItem('zzxw') || '{}') h[k] = v localStorage.setItem('zzxw', JSON.stringify(h)) } function get(k) { const h = JSON.parse(localStorage.getItem('zzxw') || '{}') return h[k] } function insert(x) { if ( $x( '/html/body/div[1]/div/div[2]/div[2]/div/div/div[2]/div/div[2]/div[1]/div/div/div[1]/div/p' ).length > 2 ) return const d = $x( '/html/body/div[1]/div/div[2]/div[2]/div/div/div[2]/div/div[2]/div[1]/div/div/div[1]/div/p[1]' ) if (!d.length) { setTimeout(() => insert(x), 50) return } const p = document.createElement('p') const t = document.createTextNode(x.name + '(' + x.teachers + ')') p.appendChild(t) d[0].after(p) } function sleep(n) { return new Promise((res) => setTimeout(res, n)) } async function wait(func) { let r = func() if (r instanceof Promise) r = await r if (r) return r await sleep(50) return await wait(func) } async function waitChange(func, value) { const r = value // eslint-disable-next-line no-constant-condition while (1) { const t = await func() if (t != r) return t await sleep(50) } } let onlinePreview = null async function getPreviewURL(storageId) { const res = await fetch( 'https://apiucloud.bupt.edu.cn/blade-source/resource/preview-url?resourceId=' + storageId ) const json = await res.json() onlinePreview = json.data.onlinePreview return json.data.previewUrl } let setClicked = false let gpage = -1 let glist = null let jsp async function main() { 'use strict' if (new URLSearchParams(location.search).get('ticket')?.length) { setTimeout(() => { location.href = 'https://ucloud.bupt.edu.cn/uclass/#/student/homePage' }, 500) } if ( location.href.startsWith( 'https://ucloud.bupt.edu.cn/uclass/course.html#/student/assignmentDetails_fullpage' ) ) { const q = new URLSearchParams(location.href) const id = q.get('assignmentId') const r = get(id) const [userid, token] = getToken() if (r) { insert(r) } else { const title = q.get('assignmentTitle') if (!id || !title) return searchCourse(userid, id, title, token).then((x) => { insert(x) set(id, x) }) } const detail = (await getDetail(id)).data const filenames = detail.assignmentResource.map((x) => x.resourceName) const urls = await Promise.all( detail.assignmentResource.map((x) => { return getPreviewURL(x.resourceId) }) ) await wait( () => $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').length > 0 ) $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').forEach( (x, index) => { const i = document.createElement('i') i.title = '预览' i.classList.add('by-icon-eye-grey') i.addEventListener('click', () => { const url = urls[index] if ( url.endsWith('.doc') || url.endsWith('.docx') || url.endsWith('.ppt') || url.endsWith('.pptx') ) window.open( 'https://view.officeapps.live.com/op/view.aspx?src=' + encodeURIComponent(url) ) else if (url.endsWith('.pdf')) window.open(url) else if (onlinePreview !== null) window.open(onlinePreview + encodeURIComponent(url)) }) x.children[3].remove() x.children[2].insertAdjacentElement('afterend', i) const i2 = document.createElement('i') i2.title = '下载' i2.classList.add('by-icon-yundown-grey') i2.addEventListener('click', () => { downloadFile(urls[index], filenames[index]) }) x.children[2].remove() x.children[1].insertAdjacentElement('afterend', i2) } ) } else if ( location.href.startsWith( 'https://ucloud.bupt.edu.cn/uclass/#/student/homePage' ) || location.href.startsWith( 'https://ucloud.bupt.edu.cn/uclass/index.html#/student/homePage' ) ) { async function getPage() { const pageText = await wait(() => document.querySelector( '#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header > div > div:nth-child(2) > div > div.banner-indicator.home-inline-block' ) ) return parseInt(pageText.innerHTML.trim().split('/')[0]) } const list = glist || (await getUndoneList()).data.undoneList let page = 1 glist = list if (list.length > 6) { page = await getPage() gpage = page if (!setClicked) { setClicked = true async function cmain() { await waitChange(getPage, gpage) main() } ( await wait(() => document.querySelector( '#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header > div > div:nth-child(2) > div > div:nth-child(2)' ) ) ).addEventListener('click', cmain); ( await wait(() => document.querySelector( '#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header > div > div:nth-child(2) > div > div:nth-child(3)' ) ) ).addEventListener('click', cmain) } } const tlist = list.slice((page - 1) * 6, page * 6) const ids = tlist.map((x) => x.activityId) const infos = await searchCourses(ids) const texts = tlist.map( (x) => infos[x.activityId].name + '(' + infos[x.activityId].teachers + ')' ) const titles = tlist.map((x) => x.activityName) await wait(() => { const texts = $x( '//*[@id="layout-container"]/div[2]/div[2]/div/div[2]/div[1]/div[3]/div[2]/div/div' ).map((x) => x.children[0].innerText) return texts.length == titles.length && texts.every((e, i) => e == titles[i]) }) const nodes = $x( '//*[@id="layout-container"]/div[2]/div[2]/div/div[2]/div[1]/div[3]/div[2]/div/div' ) for (let i = 0; i < nodes.length; i++) { if (nodes[i].children[1].children.length == 0) { const p = document.createElement('div') const t = document.createTextNode(texts[i]) p.appendChild(t) nodes[i].children[1].insertAdjacentElement('afterbegin', p) } else { nodes[i].children[1].children[0].innerHTML = texts[i] } } } else if ( location.href.startsWith( 'https://ucloud.bupt.edu.cn/uclass/course.html#/student/courseHomePage' ) ) { const site = JSON.parse(localStorage.getItem('site')) const id = site.id const resources = await getSiteResource(id) $x('//div[@class="resource-item"]/div[@class="right"]').forEach( (x, index) => { const i = document.createElement('i') i.title = '下载' i.classList.add('by-icon-download') i.classList.add('btn-icon') i.classList.add('visible') i.setAttribute( Array.from(x.attributes).filter((x) => x.localName.startsWith('data-v') )[0].localName, '' ) i.addEventListener( 'click', async (e) => { e.stopPropagation() downloadFile( await getPreviewURL(resources[index].id), resources[index].name ) }, false ) if (x.children.length) x.children[0].remove() x.insertAdjacentElement('afterbegin', i) } ) const downloadAllButton = `<div style="display: flex;flex-direction: row;justify-content: end;margin-right: 24px;margin-top: 20px;"> <button type="button" class="el-button submit-btn el-button--primary" id="downloadAllButton"> 下载全部 </button> </div>` const resourceList = $x('/html/body/div/div/div[2]/div[2]/div/div/div') const containerElement = document.createElement('div') containerElement.innerHTML = downloadAllButton resourceList[0].before(containerElement) document.getElementById('downloadAllButton').onclick = async () => { downloading = !downloading if (downloading) { document.getElementById('downloadAllButton').innerHTML = '取消下载' for (const file of resources) { if (!downloading) return await downloadFile(await getPreviewURL(file.id), file.name) } } else { document.getElementById('downloadAllButton').innerHTML = '下载全部' } } } } (function () { loadCss() jsp = loadJs('https://unpkg.com/[email protected]/nprogress.js') if (location.href.startsWith('https://ucloud.bupt.edu.cn/office/')) { const url = new URLSearchParams(location.search).get('furl') const filename = new URLSearchParams(location.search).get('fullfilename') || url const viewURL = new URL(url) if (new URLSearchParams(location.search).get('oauthKey')) { const viewURLsearch = new URLSearchParams(viewURL.search) viewURLsearch.set( 'oauthKey', new URLSearchParams(location.search).get('oauthKey') ) viewURL.search = viewURLsearch.toString() } if ( filename.endsWith('.doc') || filename.endsWith('.docx') || filename.endsWith('.ppt') || filename.endsWith('.pptx') ) location.href = 'https://view.officeapps.live.com/op/view.aspx?src=' + encodeURIComponent(viewURL.toString()) else if (filename.endsWith('.pdf')) location.href = viewURL.toString() return } main() let hash = location.hash setInterval(() => { if (location.hash != hash) { hash = location.hash main() } }, 50) })()