背景介绍
团队使用禅道记录开发过程已经有些年时间了,最近领导突然在会议上提出要求每天必须按时的点击任务的开始和结束按钮来更新任务状态。记得当时在会议上我不理智的提出人是不能做到这种程度的。紧接着又立马意识到不应该在这种场合说这样的话。同时被领导质问为什么做不到。
所以会议结束后我就在开始考虑怎么来做到这种程度,经过几天实际尝试后我证明了我自己无法做到在任务开始的时候点击开始,完成的时候点击完成并填上真实的用时。
既然我无法做到,那就想办法来做到,于是就想到了注入 JavaScript 脚本来刷自己名下的任务,自动进入 地盘-> 待处理 -> 任务页面判断是不是有任务需要完成。
实际上最终实现的功能是在中午和下班后检查今天以及今天以前是否有任务可以更新为完成状态。

实现过程
要实现网页一直刷新检查的功能首先需要装一个可以注入 JavaScript 的浏览器扩展,这里我使用的是“暴力猴”,工具有很多种,选择适合自己的。

找到需要结束的任务,并新开页面
首先我需要自动进入到“地盘-> 待处理 -> 任务”页面,来看看当前登录的账号下是否有需要结束的任务,下面是找到未完成任务的方法
// 自动完成任务1 - 在我的地盘进入的我的任务页面,找到未完成任务
var completeFlag = false;
var autoCompleteTaskMyPage = function() {
var currentHours = date.getHours();
if (!((currentHours >= 12 && currentHours < 14) || (currentHours >= 18 && currentHours <= 19))) return; // 只在中午或下午下班才操作
if(!isIn('mode=task')) return; // 当前不在我的地盘页面不响应
if (completeFlag) return;
let completeStatus = ['已完成', '已关闭']; // 已完成的任务状态
var myTaskList = $('#myTaskList tr');
var name = $('.user-profile-name').text();
var id = $('#mainContent .label-id').text(); // 完成任务编辑框上的数据id
var tr = $(`tr[data-id=${id}]`);
// 倒序查找,从最早的日期开始找
$(myTaskList.toArray().reverse()).each(function (item) {
// console.log($(item).find('td.c-user').text());
if (completeFlag) return;
var td = $(this);
var user = td.find('.c-user').text(); // 任务拥有者
var status = td.find('.c-status span').text().trim(); // 任务状态
if(name == user && !completeStatus.includes(status)) { // 只匹配当前登录用户下的任务,与不等于已完成的任务
var dealline = td.find('.text-center').text(); // 任务截止时间
// console.log(date.getFullYear() + '-' + dealline);
var taskDate = date.getFullYear() + '-' + dealline; // 完整截止时间
// 只处理小于等于今天的任务
if(new Date(taskDate) <= date) {
completeFlag = true;
// td.find('.icon-task-finish').click(); // 点击完成按钮
var dataId = td.attr('data-id'); // 任务编号
var hours = td.find('.c-hours').text(); // 任务工时
if (hours.indexOf('h')) {
hours = hours.split('h')[0];
}
console.log(status + ' - ' + dealline + ' - ' + hours);
window.location.href = `/index.php?m=task&f=finish&taskID=${dataId}&hours=${hours}&taskDate=${taskDate}`;
}
}
});
};
这里顺便提下,在很多年以前那时还没有区分前端和后端的时候,我们都是从数据库一直写到按钮事件的,前端页面对于 JavaScript “框架”主要用的 jQuery,所以发现禅道可以注入 jQuery 代码的时候还是有点感概的,我们和禅道的时间线多少还是有些交集的~~~~
所以整个 JavaScript 代码中元素查找主要使用 jQuery 实现。
第一个方法我们找到需要结束的任务,并自动点击完成按钮。这里可以看到方法的最后是跳转到一个新页面。其实这里是一卡点,因为点击完成时有一个弹窗,这个在弹窗中填写任务工时信息,如果只是弹窗也没什么问题,就是这个弹窗时使用 iframe 加载的内容,但是现在的浏览器对于 iframe 的操作被浏览器出于安全考虑给阻断了。
所以这里采用新开页面来解决填任务的详细信息来绕过 iframe 因为浏览器安全阻断的流程。

2.根据任务工时计算并填写工时信息
前面找到了任务下面就来根据任务的信息自动计算任务工时,把“本次消耗”、“实际开始”、“实际完成”以及“备注”四个输入框填上对应的内容(因为这些是必填项),再点“完成”按钮禅道就会把任务状态更新为“已完成”。
这样这个任务就算完成了,最后如果前面的操作都没出问题(进几个星期看起来没有出过问题)就再次回到“地盘-> 待处理 -> 任务”页面继续寻找下一个可以完成的任务。
// 自动完成任务2 - 在完成任务详情页面,只能通过前面1从程序跳转过来
var autoCompleteTaskFinish = function() {
if(!isIn('m=task')) return; // 当前不在完成任务详情页面不响应
var hours = queryString('hours'); // 从 url 中获取工时
var taskDate = queryString('taskDate'); // 从 url 中获取任务的日期
var isAm = date.getHours() < 13; // 是否上午
var time = isAm ? '09:05' : '13:05'; // 13点以前任务开始时间设置为9点,以后的设置为13点
var taskDateTime = new Date(`${taskDate} ${time}`);
var realStarted = `${taskDate} ${time}`;
var newHours = parseInt(taskDateTime.getHours()) + parseInt(hours);
newHours += (isAm && newHours > 12) ? 1 : 0;
var finishedDate = `${taskDate} ${newHours}:05`;
$('#currentConsumed').click().focus().val(hours).keyup(); // 设置本次消耗时间
$('#realStarted').val(realStarted); // 设置开始时间
$('#finishedDate').val(finishedDate); // 设置结束时间
$('iframe.ke-edit-iframe').contents().find('body.article-content').html(hours); // 设置 iframe 中富文本内容
$('#comment').text(hours); // 设置隐藏 comment 多行文本内容
$('button#submit').click();
setTimeout(function() { window.location.href = myTaskUrl; }, 3000); // 延时3秒跳转回我的地盘
};
到这里自动完成任务的功能就算完成了。然后在实际使用中发现一个新问题,在一段时间后会话状态会过期,就会自动退到登录页面提示登录。
这个问题也好办,当判断页面来到了登录页面,就尝试自动填写登录信息并完成点击登录,然后再回去接着工作。完美的形成了闭环。
// 自动登录
// const myTaskUrl = '/index.php?m=my&f=work&mode=task&type=assignedTo&orderBy=deadline_desc&recTotal=0&recPerPage=20&pageID=1'; // 我的地盘地址,按日期排倒序
const myTaskUrl = '/index.php?m=my&f=work&mode=task&tid='; // 我的地盘地址
var login = function () {
if(!isIn('f=login')) return;
$('#account').val('johan'); // 账号 <-------------------------
$('input[name=password]').val('123456'); // 密码 <------------
$('#submit').click(); // 点击登录
if(isIn('mode=task')) return; // 进入我的地盘
var tid = queryString('tid');
window.location.href = myTaskUrl + tid;
};
前面几次提到的判断当前是在哪个页面的功能是通过 window.location.href 获取当前的浏览器地址来实现的。前面三个方法的前面使用到的 isIn 方法下面贴出来,只有一行代码。
// 判断当前是否在指定服务器或页面
var isIn = function(str) {
return window.location.href.indexOf(str) != -1;
};
3.如何让代码自动刷新页面检查任务呢?
这个就是 setInterval 的功劳了,在注入页面的时候,使用 setInterval 将前面的两个方法加到里面去,这样就实现了自动检查的功能。
然后再来一个 count 变量记录当前检查了多少次,并且在一定次数的时候刷新一次页面,并重新注入。这是 Web 系统的特性需要不断的刷新页面来获取数据最新的状态。这里也有考虑到不给浏览器以及服务器太大的压力,刷新的频率被设置为 5 秒执行一次,这种频率对服务器来说可以算得上是无感了。
最后再留个作业:
这里用到的是 Edge 浏览器,在某一个版本的时候贴心的加入了不常用选项卡休眠以提升性能同时延长电池寿命的功能。但是这个功能和机器人冲突了,选项卡休眠后 JavaScript 代码就不会继续执行了。遇到机器人失效的小伙伴可以朝着这个方向去寻找线索。
写在最后
在思考实现功能的时候,最开始是有考虑过使用 .NET6 来实现的,因为这是我工作中主要使用的平台语言。但是这样就需要有一台服务器或自己电脑分出一部分资源来部署运行这个服务,不过这个事情本身又不能拿到台面上来说,所以没有理由申请资源,单独为这个事情挥霍几百兆内存也不是很划得来。而通过浏览器注入的方式就在平时使用的浏览器就顺便做了,权衡下来这种方式最节约资源。
贴上完整的代码压缩包下载地址,里面还附赠了一个自动统计“地盘 -> 贡献 -> 任务”页面每个月任务总数以及工时的功能,0积分免费下:JavaScript 实现自动完成禅道任务

这就是我请的机器人,正在为我工作的机器人。放在你的浏览器中,也可以为你工作。