摘要: 本文详细记录了对一款功能强大、设计精良的油猴(Tampermonkey)刷课脚本进行深度定制与优化的全过程。我们从一个简单的UI增强需求“一键全选”入手,逐步深入到脚本的核心性能瓶颈,通过引入“并发池”将刷课效率提升数倍,并最终通过修改核心判定逻辑,实现了“强制重复刷课”的终极功能。这不仅是一次代码修改的记录,更是一次充满挑战、抽丝剥茧般的逆向分析与协作解决问题的完整复盘。

缘起:一个缺失的“全选”按钮

故事始于一个看似微不足道的需求。我正在使用一款功能强大的油猴脚本来辅助完成网络课程,它允许我通过复选框勾选需要处理的课程。但当任务列表长达数十页时,缺少一个“一键全选”的功能让操作变得异常繁琐。而脚本自带的“一键取消”按钮,似乎在嘲笑我的重复劳动。于是,我萌生了自己动手、丰衣足食的想法——将这个“取消”按钮,逆向改造为我梦寐以求的“全选”按钮。

第一章:初探脚本,逆转按钮行为

我们的切入点非常直接:找到那个“取消”按钮,然后逆转它的行为。这是一个典型的逆向工程入门任务。

切入点与分析

通过油猴的管理面板,我们进入了脚本的编辑器。利用浏览器Ctrl+F搜索按钮的文本“反选所有”及其在代码中独一无二的ID unselect,我们迅速定位到了控制按钮行为的核心事件监听器。

原始的“取消”逻辑非常清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 原始的“取消”逻辑
uns.on('click', () => {
// 核心动作:调用一个方法清空已选列表
this.mission.emptySelection();
// ... (一些更新UI的代码)
});```
它的心脏就是调用 `this.mission.emptyselection()` 来清空一个名为 `selection_arr` 的已选课程数组。

### 逆向改造:从“清空”到“填满”

要实现“全选”,我们只需要反其道而行之——用所有课程**填满**这个数组。通过分析UI代码中另一个名为`fresh`的刷新按钮,我们发现所有课程的复选框(Checkbox)都有一个共同的CSS类名 `.course-selection`。

一个绝妙的方案诞生了:我们可以模拟用户手动点击所有未选中的复选框,利用脚本已有的“点击即添加”逻辑,来实现跨越多个分页的真正“一键全选”。

#### 代码对比

```javascript
// 【修改前】 - 取消全选
uns.on('click', () => {
this.mission.emptySelection();
});

// 【修改后】 - 一键全选
// 别忘了把按钮文本也改掉,例如:uns.text("一键全选所有课程");
uns.on('click', () => {
// 1. 找到页面上所有课程的复选框元素
let all_checkboxes = $(`.course-selection`);

// 2. 遍历每一个复选框
all_checkboxes.each(function() {
// 3. 检查这个复选框是否还未被选中
if (!$(this).prop('checked')) {
// 4. 如果未被选中,就模拟一次点击操作来选中它
$(this).click();
}
});
});

至此,第一个目标轻松达成。小小的成就感燃起了我们进一步探索的欲望。

第二章:效率革命,从串行到三并发的性能飞跃

“一键全选”解决了选择问题,但新的瓶颈随之而来。当选中上百个课程时,脚本默认的“刷完一个再刷下一个”的串行处理方式,效率极其低下,整个过程可能需要数十分钟。为了实现“一杯咖啡,任务完成”的终极理想,我们必须对脚本的核心执行流程进行性能优化,引入并行处理

定位性能瓶颈

通过搜索getSelections(获取已选列表)和FinishCourse(执行单个课程)等关键词,我们找到了负责执行刷课的循环。这是一个典型的 for await...of 异步迭代循环。

1
2
3
4
5
6
7
// 原始的串行循环
// 它会遍历一个控制器 ficCourse,该控制器包含了所有待刷的课程
for await (let courseTask of this.ficCourse) {
// 这里的 await 是效率瓶颈,它会阻塞循环,直到当前课程刷完
let result = await courseTask.FinishCourse();
// ... 更新UI进度 ...
}

await 关键字在循环体内部,导致了整个流程的阻塞。我们的任务,就是打破这个枷锁。

引入“并发池”:在效率与风险间走钢丝

过于激进的并行(比如一次性发起100个请求)极有可能触发服务器的风控机制,导致IP或账号被封。因此,我们不能简单地用Promise.all()“万箭齐发”。

一个更稳妥的方案是实现一个能控制并发数量的“并发池”。经过实验,我们发现3是一个既能大幅提升效率又不易触发风控的完美平衡点。

代码对比

我们用一个调度器函数 run() 代替了原始的 for 循环,实现了这个并发池。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 【修改前】 - 串行的 for...await...of 循环
for await (let courseTask of this.ficCourse) {
await courseTask.FinishCourse();
// ... 更新进度 ...
}

// 【修改后】 - 带有并发控制的调度器
const concurrency_limit = 3; // 设置并发上限
const tasks = [...this.ficCourse]; // 复制任务队列
let running_tasks = 0;
let completed_tasks = 0;

// 调度器函数
const run = () => {
// 当有空闲“槽位”且还有任务时,启动新任务
while (running_tasks < concurrency_limit && tasks.length > 0) {
const task = tasks.shift();
running_tasks++;

// 更新UI
btn.text(`进度: ${completed_tasks}/${tasks.length} | 并发: ${running_tasks}`);

// 执行任务,不使用await
task.FinishCourse()
.catch(error => console.error("单个任务失败:", error))
.finally(() => {
running_tasks--;
completed_tasks++;

// 一个任务结束后,立刻尝试启动下一个
run();
});
}

// 所有任务都完成后收尾
if (tasks.length === 0 && running_tasks === 0) {
// ... 显示完成信息 ...
}
};

run(); // 首次启动

这次改造的效果是惊人的。原本需要半小时的任务,现在只需要几分钟就能完成。脚本的执行效率得到了质的飞跃,从“单车道”变成了“三车道高速公路”。

最终章:打破规则,实现“强制重复刷课”

最后一个挑战来自于脚本自身的一个“智能”设计:它会自动跳过那些已经完成的课程。但在某些情况下,我们可能需要重新刷一遍课程以更新学习记录。因此,我们的终极目标是:打破这个规则,实现无差别重复刷课

探索“跳过”逻辑的根源

通过深入阅读FinishCourseService的源码,我们终于找到了控制“跳过”行为的“总开关”。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在 FinishCourseService 的 FinishCourse 方法内部
async FinishCourse() {
// ...

// 【关键判定】
// this.ratio 是在初始化时从平台获取的课程完成度(0到1)
if (this.ratio == 1) { // 如果完成度为100%
// 返回-2,上层循环接收到后,便知道该跳过此任务
return -2;
}

// ... 后续的日志欺骗与提交逻辑 ...
}

逻辑非常简单:在执行任何耗时操作之前,先检查课程完成度ratio。如果为1,则立刻返回一个“跳过”信号-2

精确的手术:注释的力量

要实现重复刷课,我们只需要让这个 if 判断永远失效。最简单、最安全、最优雅的方式,就是注释掉它

代码对比

1
2
3
4
5
6
7
8
9
10
// 【修改前】 - 自动跳过已完成
if (this.ratio == 1) {
return -2;
}

// 【修改后】 - 强制重复刷
// 通过注释,我们废除了完成状态检查
// if (this.ratio == 1) {
// return -2;
// }

仅仅添加了两个//,我们就赋予了脚本全新的行为模式。现在,无论课程之前是否完成,脚本都会一视同仁地为它们重新执行完整的刷课流程,实现了我们最终的目标。

结语

这次对油猴脚本的深度改造,是一次从用户到开发者的完美蜕变。我们不仅实现了所有既定目标,更在这个过程中,深入理解了一款优秀脚本的架构设计,并通过抽丝剥茧般的调试,锻炼了解决实际问题的能力。

最重要的收获是,面对看似复杂的“黑盒子”,只要我们有足够的耐心,善用分析工具,并采用科学的调试方法,就一定能洞悉其内在的逻辑,并最终让它为我所用。希望这次的分享,能给每一位热爱折腾、勇于探索的你,带来一些启发和乐趣。
```