From d80c490f48b5d460f4d9b885388593da5680dd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E6=98=9F=E7=8E=AF=E6=B5=B7?= <3032732426@qq.com> Date: Tue, 6 Oct 2026 02:38:21 +0800 Subject: [PATCH] Advance when learning is complete and the platform exam is passed --- 2倍速脚本使用说明.md | 10 ++++----- README.md | 14 ++++++------ icampus-study-helper.user.js | 33 ++++++++++++++-------------- package.json | 2 +- persistent-run.test.cjs | 42 +++++++++++++++++++++++++++--------- 5 files changed, 63 insertions(+), 38 deletions(-) diff --git a/2倍速脚本使用说明.md b/2倍速脚本使用说明.md index 282e5f4..f423d42 100644 --- a/2倍速脚本使用说明.md +++ b/2倍速脚本使用说明.md @@ -1,4 +1,4 @@ -# 学员中心 · 2倍速学习助手 v1.3.0 +# 学员中心 · 2倍速学习助手 v1.3.1 适用网页:https://icampus.yunyinsh.com/course/ @@ -7,9 +7,9 @@ 1. 在篡改猴中停用旧的“学员中心 · 本地网络诊断”脚本,避免误触倍速测试。 2. 打开篡改猴管理面板 → 实用工具 → 从文件导入,选择 `icampus-study-helper.user.js`,按提示安装/更新。同名、同 namespace 的1.0版本应被更新,不要同时启用两份。也可以在原脚本编辑器中用该文件的全部内容替换并保存。 3. 刷新课程网页,让助手能从页面加载时读取平台返回的课程信息。 -4. 左下角出现“2倍速学习助手 · 1.3.0”,点击“开始”。默认从列表首个未完成课程开始;已经进入学习页时,先完成当前课程。 +4. 左下角显示“2倍速学习助手 · 1.3.1”。已启动的标签页刷新后会接续;首次使用点击“开始”。默认从列表首个未完成课程开始;已经进入学习页时,先完成当前课程。 5. 可选择“仅当前课程”。开始后始终保持启用,等待评分或处理异常不要求再次点开始;刷新同一标签页也会接续。只有按“人工取消”才关闭助手并暂停当前控制的视频。 -6. 默认勾选“自动测验,达到100%才继续”。先自动作答、交卷并重考,测验达到100%后再继续本门视频或下一课程。也可点击“只做本门测验”。 +6. 默认勾选“自动测验,按平台合格线通过”。自动作答、交卷,未合格才重考;合格后继续视频。学习进度100%且测验合格后自动下一门,不要求测验得分100%。也可点击“只做本门测验”。 7. 默认勾选“切标签页继续播放”。先启动助手,再切到其他标签页;无需一直把课程页放在前台。关闭开关会恢复网站原有的隐藏页面处理。 ## 已实现 @@ -27,7 +27,7 @@ ## 当前边界 -- 当前现场试卷显示不限次数,新版不设置脚本的重考次数上限。严格达到100%才放行;即使平台已显示合格,不满100%也保持等待重考,不切换视频或课程。 +- 当前现场试卷显示不限次数,新版不设置脚本的重考次数上限。测验按平台判定是否合格;学习进度100%且测验合格就换课,例如75%达到60%的合格线也会继续。 - 如果平台返回次数耗尽、入口不可用或没有逐题反馈,保持启用并等待处理,不反复提交完全相同的答案。 - 图片题、材料题、填空、问答,以及无法唯一识别选项的题需要手动作答。单选/判断/多选支持每题最多8个不同文字选项。 - 现场已确认电脑端交卷在侧栏、手机端底部按钮被隐藏;新版本已经修正选择器,但尚未安装新版完成真实自动重考到100%的端到端验证。 @@ -42,6 +42,6 @@ ## 验证情况 -本地语法检查及61项行为测试通过(16项播放/进度测试、13项测验测试、13项媒体恢复测试、12项后台播放测试、7项持续运行测试)。模拟页面完整跑通开始、选择、电脑端交卷、确认、逐题反馈、重试,并在第三次达到100%;验证了正确题不重复改选、低于100%不放行、超过旧重试上限仍可继续、刷新接续及人工取消。此前已实测网页原生2倍速可以持续播放,并收到平台保存响应。 +本地语法检查及64项行为测试通过(16项播放/进度测试、13项测验测试、13项媒体恢复测试、12项后台播放测试、10项持续运行与完成条件测试)。新增“学习100%+测验75%合格后自动下一课程”、学习未完成继续视频、学习进度不能替代测验合格等回归。此前已实测网页原生2倍速可以持续播放,并收到平台保存响应。 新助手还需要在安装后验证实际页面运行;尚未完成整门课程或全部34门课程的自动播放实测,不能据此保证最终学分或所有课程结构兼容。 diff --git a/README.md b/README.md index 62e49ea..ceabd45 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,20 @@ # 学员中心 · 2倍速学习助手 -适用于 `https://icampus.yunyinsh.com/course/` 的篡改猴脚本。当前版本 **1.3.0**。 +适用于 `https://icampus.yunyinsh.com/course/` 的篡改猴脚本。当前版本 **1.3.1**。 ## 安装 -下载 `icampus-study-helper.user.js`,在篡改猴管理面板的“实用工具 → 从文件导入”中安装或更新原脚本。刷新课程页,确认面板版本为1.3.0,点击左下角“开始”。不要同时启用旧诊断脚本。 +下载 `icampus-study-helper.user.js`,在篡改猴管理面板的“实用工具 → 从文件导入”中安装或更新原脚本。刷新课程页,确认面板版本为1.3.1。已启动的标签页会自动接续;首次使用点击“开始”。不要同时启用旧诊断脚本。 默认按课程列表顺序处理,支持仅当前课程,以及只做本门测验。首次安装等待点击开始;开始后保持启用,刷新同一标签页也会接续,只有“人工取消”才清除运行状态。 -## 1.3.0 自动交卷与持续运行 +## 1.3.1 完成条件、自动交卷与持续运行 电脑端的“交卷”在侧栏,手机端的底部交卷按钮在电脑上隐藏。旧版只查找底部按钮,导致已答完却无法提交。新版选择当前实际可见的交卷按钮,并自动确认交卷。 -测验必须达到 **100%** 才允许继续本门视频或下一课程。低于100%时,根据逐题反馈保存正确答案、排除错误答案并重新测验;不设脚本自己的重考次数上限。当前现场试卷显示“答题次数:不限”。若入口暂不可用,保留已启动状态等待,不跳过该测验。 +继续下一课程的条件是 **学习进度100%+平台判定测验合格**,不要求测验得分100%。例如学习进度100%、测验75%、合格线60%,应继续下一门;学习进度75%而测验已合格,则继续学习视频。测验是否合格以平台的明确判定为准,不硬编码合格线。 + +未合格时根据逐题反馈保存正确答案、排除错误答案并重新测验,不设置脚本自己的重考次数上限。已合格不再重考;得分不足100%的整卷不会误记为全部答案正确。 “运行中”和“等待恢复”都属于已启动。网络异常、等待评分、当前范围完成或需要人工处理时保持启用;自动定期检查或在页面变化后接续。需要停止时点“人工取消”。手动暂停视频后会等待用户恢复视频,不主动抢占暂停操作。 @@ -44,7 +46,7 @@ ## 验证 -运行 `npm test`,无需安装依赖。当前61项本地行为测试覆盖播放、进度保存、测验循环、媒体加载恢复、后台模式、严格100%条件和刷新接续。测验模拟包含隐藏的手机端按钮与可见电脑端按钮,验证自动选择、交卷、确认、重试直至100%。 +运行 `npm test`,无需安装依赖。当前64项本地行为测试覆盖播放、进度保存、测验循环、媒体加载恢复、后台模式、学习进度与测验合格的独立判断、刷新接续。包含“学习100%+测验75%合格→返回列表→打开下一课程”的完整模拟回归。 现场已确认原问题出现时第二段视频处于 `readyState=0` 且旧版已超时停止;修复通过模拟播放器与页面回归。尚未安装修复版验证真实课程完整连播。 @@ -52,6 +54,6 @@ ## 限制 -计入进度、课程完成和重试能力均以平台返回结果为准。重考入口暂不可用时会等待;即使平台显示“合格”,不足100%仍不会放行。图片、材料、填空和问答题需要人工处理。请保持电脑唤醒。 +计入进度、课程完成和重试能力均以平台返回结果为准。未合格且重考入口暂不可用时会等待;已合格则继续核对学习进度。图片、材料、填空和问答题需要人工处理。请保持电脑唤醒。 更多说明见 [使用说明](2倍速脚本使用说明.md)。 diff --git a/icampus-study-helper.user.js b/icampus-study-helper.user.js index 4761d5f..0cd14d6 100644 --- a/icampus-study-helper.user.js +++ b/icampus-study-helper.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name 学员中心 · 2倍速学习助手 // @namespace local.icampus.study -// @version 1.3.0 +// @version 1.3.1 // @description 支持切换标签页继续2倍速播放与保存进度,自动测验及错题重试。 // @match https://icampus.yunyinsh.com/course/* // @run-at document-start @@ -28,14 +28,19 @@ function bodyJSON(body) { try { return typeof body === 'string' && body.length < 65536 ? JSON.parse(body) : null; } catch { return null; } } - function examPerfect(c) { return !!c && Number(c.examScorePercent)===100 && (c.examPassed===true || c.examPassed==='true'); } - function courseDone(c) { return !!c && (c.status === 'COMPLETED' || !!c.completedAt) && (!c.examRequired || examPerfect(c)); } + function examPassed(c) { return !!c && (c.examPassed===true || c.examPassed==='true'); } + function courseDone(c) { + if(!c) return false; + const hasProgress=c.progressPercent!==null && c.progressPercent!==undefined && c.progressPercent!==''; + const learningDone=hasProgress ? Number(c.progressPercent)>=100 : c.status==='COMPLETED' || !!c.completedAt; + return learningDone && (!c.examRequired || examPassed(c)); + } function videoPlan(detail) { if (!detail?.course || !Array.isArray(detail.resources)) return {kind: 'wait'}; const resources = [...detail.resources].sort((a, b) => a.sortNo - b.sortNo); const next = resources.find(r => r.completed !== true); if (next) return {kind: next.type === 'VIDEO' ? 'video' : 'manual', resource: next}; - if (detail.course.examRequired && !examPerfect(detail.course)) return {kind: 'quiz'}; + if (detail.course.examRequired && !examPassed(detail.course)) return {kind: 'quiz'}; return {kind: 'finish'}; } function resolveCards(cards, courses) { @@ -92,7 +97,7 @@ return m ? {kind:'card', paperId:m[1]} : null; } catch { return null; } } - const core = {normalize, route, endpoint, bodyJSON, courseDone, examPerfect, videoPlan, resolveCards, leaseAvailable, + const core = {normalize, route, endpoint, bodyJSON, courseDone, examPassed, videoPlan, resolveCards, leaseAvailable, questionKey, selectionKey, chooseAnswer, learnCard, quizEndpoint}; if (typeof module === 'object' && module.exports && typeof window === 'undefined') { module.exports = core; return; } if (window.top !== window.self || document.getElementById('icampus-study-helper')) return; @@ -406,13 +411,9 @@ const percent = result.querySelector('.quiz-result__score strong')?.textContent.trim(); const score=percent && /^\d+(\.\d+)?%$/.test(percent) ? Number(percent.replace('%','')) : course?.examScorePercent; if(course && score!==undefined) course.examScorePercent=score; - if(Number(score)===100) { - log('测验已达到100%,允许继续。'); - if(quizOnly) return park('本门测验已达到100%,保持已启动,等待后续操作。',{passive:true}); - quiz=null; return; - } - // A lower platform pass mark does not satisfy the user's 100% gate. - if(!quizButton(root,'重新测验')) return park(`当前正确率${score ?? '未知'}%,等待重考入口;达到100%前不切换视频或课程。`); + log(`平台已确认测验合格(${score ?? '未显示'}%),继续检查学习进度。`); + if(quizOnly) return park('本门测验已合格,保持已启动,等待后续操作。',{passive:true}); + quiz=null; return; } if (quiz.phase === 'opening' || quiz.phase === 'retrying') return quizWait('正在打开测验…'); const exam = exams.get(id); @@ -581,7 +582,7 @@ return; } const plan = videoPlan(detail); - if(detail.course.examRequired && !examPerfect(detail.course)) { + if(detail.course.examRequired && !examPassed(detail.course)) { const gatedVideo=active?.video || currentVideo(); if(gatedVideo && !gatedVideo.paused) gatedVideo.pause(); active=null; @@ -730,14 +731,14 @@ host.style.cssText = 'position:fixed;left:16px;bottom:16px;z-index:2147483646'; panel = host.attachShadow({mode: 'open'}); panel.innerHTML = ` -
2倍速学习助手 · 1.3.0

+
2倍速学习助手 · 1.3.1

- +
- 启动后持续运行与等待;只有人工取消才关闭。测验100%后继续视频。 + 学习进度100%且测验合格后换课。持续运行,人工取消才关闭。
运行记录
`; mode = panel.querySelector('select'); autoQuiz = panel.querySelector('[data-quiz]'); diff --git a/package.json b/package.json index d31e343..8a0de71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "e-learning-assistant", - "version": "1.3.0", + "version": "1.3.1", "private": true, "description": "学员中心2倍速播放和测验辅助脚本", "scripts": { diff --git a/persistent-run.test.cjs b/persistent-run.test.cjs index 21a037c..91cc799 100644 --- a/persistent-run.test.cjs +++ b/persistent-run.test.cjs @@ -7,23 +7,45 @@ async function test(name,fn){await fn();passed++;console.log(`PASS ${name}`);} function mount(h){const nodes=new Map();const panel={querySelector(s){if(!nodes.has(s))nodes.set(s,{textContent:'',value:'all',checked:true});return nodes.get(s);}}; h.context.document.body={append(){}};h.context.document.createElement=()=>({style:{},attachShadow:()=>panel});h.api.mount();return nodes;} (async()=>{ - await test('a platform pass below 100 never counts as a completed course',()=>{ - const d=detail();d.course.status='COMPLETED';d.course.examScorePercent=60;d.resources.forEach(r=>r.completed=true); - assert.equal(core.courseDone(d.course),false);assert.equal(core.videoPlan(d).kind,'quiz'); - d.course.examScorePercent=100;assert.equal(core.courseDone(d.course),true); + await test('100 percent learning with a 75 percent passing exam is complete',()=>{ + const d=detail();d.course.status='COMPLETED';d.course.progressPercent=100;d.course.examScorePercent=75;d.resources.forEach(r=>r.completed=true); + assert.equal(core.courseDone(d.course),true);assert.equal(core.videoPlan(d).kind,'finish'); + d.course.examPassed=false;assert.equal(core.courseDone(d.course),false);assert.equal(core.videoPlan(d).kind,'quiz'); }); - await test('a course below 100 starts its quiz before playing or switching video',async()=>{ + await test('an unpassed exam starts its quiz before playing or switching video',async()=>{ const h=harness();let starts=0;const c=h.api.details.get(ID).course;c.examPassed=false;c.examScorePercent=0; const button={textContent:'开始测验',disabled:false,getClientRects:()=>[{}],click(){starts++;}}; h.selectors.set('.course-quiz',{querySelector:()=>null,querySelectorAll:s=>s==='button'?[button]:[]}); h.api.begin();await h.api.tickCourse(ID);assert.equal(starts,1);assert.equal(h.video.playCount,0);assert.equal(h.buttons[1].clicks,0);assert.equal(h.video.paused,true); }); - await test('a low passing score with a missing retake entry stays armed and does not advance',async()=>{ - const h=harness();const c=h.api.details.get(ID).course;c.examScorePercent=80; - const result={getClientRects:()=>[{}],querySelector:s=>s==='.quiz-result__status.is-pass'?{}:s==='.quiz-result__score strong'?{textContent:'80%'}:null}; + await test('a low passing score needs no retake entry and allows return to the list',async()=>{ + const h=harness();const d=h.api.details.get(ID);const c=d.course;c.examScorePercent=75;c.progressPercent=100;d.resources.forEach(r=>r.completed=true); + let backs=0;h.selectors.set('.van-nav-bar__left',{click(){backs++;}}); + const result={getClientRects:()=>[{}],querySelector:s=>s==='.quiz-result__status.is-pass'?{}:s==='.quiz-result__score strong'?{textContent:'75%'}:null}; h.selectors.set('.course-quiz',{querySelector:s=>s==='.quiz-result'?result:null,querySelectorAll:()=>[]}); - h.api.begin();await h.api.tickCourse(ID);assert.equal(h.api.state.armed,true);assert.equal(h.api.state.running,false); - assert.match(h.api.state.status,/100%/);assert.equal(h.video.playCount,0);assert.equal(h.buttons[1].clicks,0); + h.api.begin();h.api.tickQuiz(ID);await h.api.tickCourse(ID);assert.equal(h.api.state.armed,true);assert.equal(h.api.state.running,true); + assert.equal(backs,1);assert.equal(h.video.playCount,0);assert.equal(h.buttons[1].clicks,0); + }); + await test('learning progress and exam score cannot substitute for each other',()=>{ + const c={...detail().course,progressPercent:75,examScorePercent:100};assert.equal(core.courseDone(c),false); + c.progressPercent=100;c.examPassed=false;assert.equal(core.courseDone(c),false); + c.examPassed=true;c.examScorePercent=60;assert.equal(core.courseDone(c),true); + c.progressPercent=99;c.status='COMPLETED';assert.equal(core.courseDone(c),false); + }); + await test('a passed 75 percent exam with unfinished learning continues its videos',async()=>{ + const h=harness();const c=h.api.details.get(ID).course;c.progressPercent=75;c.examScorePercent=75; + h.api.begin();await h.api.tickCourse(ID);h.advance(1000);await h.api.tickCourse(ID); + assert.equal(h.video.playCount,1);assert.equal(core.courseDone(c),false); + }); + await test('100 percent learning and a 75 percent pass advance from the result to the next course',async()=>{ + const h=harness(),d=h.api.details.get(ID);d.course.progressPercent=100;d.course.examScorePercent=75;d.course.status='COMPLETED'; + d.resources.forEach(r=>r.completed=true); + h.selectors.set('.van-nav-bar__left',{click(){h.context.location.hash='#/my-course';}}); + const clicks=[0,0];const names=['课程一','课程二']; + h.selectors.set('article.course-row',names.map((name,index)=>({getClientRects:()=>[{}],querySelector:()=>({textContent:name}),click(){clicks[index]++;}}))); + h.api.begin();await h.api.tickCourse(ID);h.advance(1000); + h.api.receive({id:'packages',tail:''},{bizCode:0,data:[{courses:[d.course,{id:'123',courseName:'课程二',progressPercent:0,examRequired:true,examPassed:false,status:'NOT_STARTED'}]}]},200); + await h.api.tick();assert.deepEqual(clicks,[0,1]);assert.equal(h.api.state.running,true);assert.equal(h.api.state.armed,true); }); await test('temporary waits retain the started button state and retry automatically',async()=>{ const h=harness();const ui=mount(h);ui.get('[data-start]').onclick();h.api.park('网络等待');