// ==UserScript==
// @name monkey-yunxiao
// @namespace npm/vite-plugin-monkey
// @version 0.0.8
// @author monkey
// @description 云效脚本
// @license MIT
// @icon data:image/svg+xml,%3Csvg%20width%3D%22240%22%20height%3D%22240%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cdefs%3E%3ClinearGradient%20x1%3D%2250.007%25%22%20y1%3D%2299.839%25%22%20x2%3D%2250.007%25%22%20y2%3D%22.339%25%22%20id%3D%22a%22%3E%3Cstop%20stop-color%3D%22%23006ad4%22%20stop-opacity%3D%22.5%22%20offset%3D%220%25%22%2F%3E%3Cstop%20stop-color%3D%22%23006ad4%22%20stop-opacity%3D%22.2%22%20offset%3D%22100%25%22%2F%3E%3C%2FlinearGradient%3E%3ClinearGradient%20x1%3D%2250.035%25%22%20y1%3D%22-.079%25%22%20x2%3D%2250.035%25%22%20y2%3D%2299.929%25%22%20id%3D%22b%22%3E%3Cstop%20stop-color%3D%22%23006ad4%22%20offset%3D%220%25%22%2F%3E%3Cstop%20stop-color%3D%22%23006ad4%22%20stop-opacity%3D%22.5%22%20offset%3D%22100%25%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Crect%20width%3D%22240%22%20height%3D%22240%22%20rx%3D%2212%22%2F%3E%3Cg%20transform%3D%22translate(24%2024)%22%20fill-rule%3D%22nonzero%22%3E%3Cpath%20d%3D%22m192%20191.774-67.621-89.901%2048.997-67.087-33.921-26.202-69.395%2093.74%2060.748%2080.64c4.213%205.648%2010.642%208.81%2017.737%208.81H192Z%22%20fill%3D%22url(%23a)%22%2F%3E%3Cellipse%20fill%3D%22%23006ad4%22%20cx%3D%22156.527%22%20cy%3D%2221.685%22%20rx%3D%2221.284%22%20ry%3D%2221.685%22%2F%3E%3Cpath%20d%3D%22M124.379%20101.873%2061.413%2018.296c-4.212-5.647-10.642-8.809-17.736-8.809H0l69.838%2092.838L3.77%20192h43.012c7.094%200%2013.524-3.388%2017.736-9.035l59.862-81.092Z%22%20fill%3D%22url(%23b)%22%2F%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E
// @match https://devops.aliyun.com/projex/*
// @match https://devops.aliyun.com/workbench*
// @require https://lf9-cdn-tos.bytecdntp.com/cdn/expire-10-y/rxjs/7.5.4/rxjs.umd.min.js
// @require https://lf9-cdn-tos.bytecdntp.com/cdn/expire-10-y/dayjs/1.10.8/dayjs.min.js
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
(t=>{if(typeof GM_addStyle=="function"){GM_addStyle(t);return}const e=document.createElement("style");e.textContent=t,document.head.append(e)})(" ._status-button-progress_cn3xm_1{position:absolute;bottom:0;left:0;width:100%;height:2px;background:red;transition:width .5s}._working-hours-tips_cn3xm_11{color:red;white-space:nowrap;height:100%;display:flex;align-items:center} ");
(function (rxjs, dayjs) {
'use strict';
class ChainOfResponsibility {
handlers;
constructor() {
this.handlers = [];
}
add(handler) {
this.handlers.push(handler);
handler.init?.();
return this;
}
handleApi(params) {
for (const handler of this.handlers) {
if (handler.match(params.triggerURLPath)) {
const task = handler.apiMaps.get(params.path);
if (task) {
console.log(
`通过接口请求,触发了脚本: ${params.path} ===> ${handler.name}`
);
task(params.responseJSON);
}
}
}
}
}
const getUserInfo = /* @__PURE__ */ (() => {
let promise;
return async () => {
if (promise) {
return promise;
}
promise = fetch(`https://devops.aliyun.com/uiless/api/sdk/users/me`, {
headers: {
"Content-Type": "application/json"
},
method: "GET",
credentials: "include"
}).then((res) => res.json()).then((res) => {
return {
name: res.result.user.name,
identifier: res.result.user.id
};
});
return promise;
};
})();
let isCheat = false;
const initKeyBind = () => {
const konami$ = [];
const sub = rxjs.fromEvent(document, "keydown").pipe(
rxjs.filter((e) => {
konami$.push(e.key.toLocaleLowerCase());
if (konami$.length > 10) {
konami$.shift();
}
return konami$.join("") === "arrowuparrowuparrowdownarrowdownarrowleftarrowrightarrowleftarrowrightba";
})
).subscribe(() => {
isCheat = true;
sub.unsubscribe();
});
};
const getAllMembers = /* @__PURE__ */ (() => {
let allMembers;
return async () => {
if (allMembers) {
return allMembers;
}
allMembers = fetch(
"https://devops.aliyun.com/projex/api/workspace/space/recommend/member/list?pageSize=100&withDeletedAndDisabled=false",
{
credentials: "include"
}
).then((res) => res.json()).then((res) => res.result);
return allMembers;
};
})();
const style = {
"status-button-progress": "_status-button-progress_cn3xm_1",
"working-hours-tips": "_working-hours-tips_cn3xm_11"
};
const yunxiao = "data:image/svg+xml,%3csvg%20width='240'%20height='240'%20xmlns='http://www.w3.org/2000/svg'%3e%3cdefs%3e%3clinearGradient%20x1='50.007%25'%20y1='99.839%25'%20x2='50.007%25'%20y2='.339%25'%20id='a'%3e%3cstop%20stop-color='%23006ad4'%20stop-opacity='.5'%20offset='0%25'/%3e%3cstop%20stop-color='%23006ad4'%20stop-opacity='.2'%20offset='100%25'/%3e%3c/linearGradient%3e%3clinearGradient%20x1='50.035%25'%20y1='-.079%25'%20x2='50.035%25'%20y2='99.929%25'%20id='b'%3e%3cstop%20stop-color='%23006ad4'%20offset='0%25'/%3e%3cstop%20stop-color='%23006ad4'%20stop-opacity='.5'%20offset='100%25'/%3e%3c/linearGradient%3e%3c/defs%3e%3cg%20fill='none'%20fill-rule='evenodd'%3e%3crect%20width='240'%20height='240'%20rx='12'/%3e%3cg%20transform='translate(24%2024)'%20fill-rule='nonzero'%3e%3cpath%20d='m192%20191.774-67.621-89.901%2048.997-67.087-33.921-26.202-69.395%2093.74%2060.748%2080.64c4.213%205.648%2010.642%208.81%2017.737%208.81H192Z'%20fill='url(%23a)'/%3e%3cellipse%20fill='%23006ad4'%20cx='156.527'%20cy='21.685'%20rx='21.284'%20ry='21.685'/%3e%3cpath%20d='M124.379%20101.873%2061.413%2018.296c-4.212-5.647-10.642-8.809-17.736-8.809H0l69.838%2092.838L3.77%20192h43.012c7.094%200%2013.524-3.388%2017.736-9.035l59.862-81.092Z'%20fill='url(%23b)'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e";
const match$1 = (path) => {
return (
// 任务视图
/^\/projex\/project\/.+\/task/.test(path) || // 工作项视图
/^\/projex\/workitem/.test(path) || // 工作台视图
/^\/workbench$/.test(path)
);
};
const apiMaps$1 = /* @__PURE__ */ new Map([
[
"/projex/api/workitem/workitem/list",
(data) => {
rxjs.interval(100).pipe(
// 查询到元素存在后,停止轮询,并返回元素
rxjs.map(() => {
let dom = document.querySelector(
"#AONE_MY_WORKITEM_CARD .next-table-inner"
);
let col = 1;
if (dom) {
return {
dom,
col
};
}
dom = document.querySelector(
".workitemListMainAreaWrap .next-table-inner"
);
if (dom) {
col = findColsNum(dom, "状态");
return {
dom,
col
};
}
return void 0;
}),
rxjs.first((data2) => !!data2),
rxjs.takeUntil(rxjs.timer(500))
).subscribe({
next: ({ dom: target, col }) => {
if (target.getAttribute("init-progress")) {
console.log("已经初始化过了");
return;
}
target.setAttribute("init-progress", "true");
if (col) {
const elList = target.querySelectorAll(
`.next-table-body tr td[data-next-table-col="${col}"] button`
);
elList.forEach((el, i) => {
const dataItem = data.result[i];
if (dataItem.workitemType.name !== "任务") return;
el.style.position = "relative";
const div = document.createElement("div");
div.classList.add(style["status-button-progress"]);
el.appendChild(div);
const parentIdentifier = dataItem?.parentIdentifier;
parentIdentifier && queryParentAndUpdateEl(parentIdentifier, div, dataItem);
});
}
}
});
}
]
]);
function findColsNum(target, title) {
const thList = target.querySelectorAll(
".next-table-header th"
);
for (let i = 0; i < thList.length; i++) {
if (thList[i].innerText === title) {
return i;
}
}
return void 0;
}
let notCompletedTaskMap = /* @__PURE__ */ new Map();
async function queryOtherTaskProgress(parentIdentifier, { identifier }) {
const res = await fetch(
`https://devops.aliyun.com/projex/api/workitem/v2/workitem/${parentIdentifier}/relation/workitem/list/by-relation-category?category=PARENT_SUB&isForward=true&_input_charset=utf-8`,
{
headers: {
"Content-Type": "application/json"
},
method: "GET",
credentials: "include"
}
).then((res2) => res2.json());
if (res.code !== 200) {
return 0;
}
const taskList = res.result?.filter((item) => item.workitemTypeName === "任务").filter((item) => item.identifier !== identifier);
if (!taskList?.length) {
return 100;
}
const progress = taskList.length * 100;
let completed = 0;
taskList.forEach((task) => {
const progressStr = task.fieldValueVOList.find(
(field) => field.fieldIdentifier === "progress"
)?.value;
if (progressStr === "0.1") return;
const n = Number(progressStr);
if (n) {
completed += n;
}
});
return completed / progress * 100;
}
async function queryParentAndUpdateEl(parentIdentifier, dom, dataItem) {
const progress = await queryOtherTaskProgress(parentIdentifier, {
identifier: dataItem.identifier
});
dom.style.width = `${progress}%`;
const userInfo = await getUserInfo();
const userId = userInfo.identifier;
if (userId === dataItem.assignedTo.identifier) {
if (progress === 100) {
notCompletedTaskMap.delete(dataItem.identifier);
} else {
if (!notCompletedTaskMap.has(dataItem.identifier)) {
notCompletedTaskMap.set(dataItem.identifier, {
subject: dataItem.subject,
identifier: dataItem.identifier,
spaceIdentifier: dataItem.spaceIdentifier,
parentIdentifier
});
}
}
}
}
async function notificationTask([
identifier,
{ subject, parentIdentifier, spaceIdentifier }
]) {
try {
const progress = await queryOtherTaskProgress(parentIdentifier, {
identifier
});
if (progress === 100) {
notCompletedTaskMap.delete(identifier);
new Notification("其他任务进度已完成", {
body: `任务:${subject} 的其他任务已完成,点击查看需求详情`,
icon: yunxiao,
requireInteraction: true
}).addEventListener("click", () => {
window.open(
`https://devops.aliyun.com/projex/project/${spaceIdentifier}/req/${parentIdentifier}`
);
});
}
} catch (e) {
console.error("查询任务进度失败");
}
}
rxjs.interval(1e3 * 60 * 10).pipe(
rxjs.mergeMap(
() => rxjs.from([...notCompletedTaskMap.entries()]).pipe(
rxjs.mergeMap(notificationTask, 5)
)
)
).subscribe();
const name$1 = "任务进度条";
const taskHandler = {
match: match$1,
apiMaps: apiMaps$1,
name: name$1
};
const workClassName = style["working-hours-tips"];
async function getWorkingHours(userId) {
if (userId) ; else {
const { identifier } = await getUserInfo();
userId = identifier;
}
const endTime = dayjs().format("YYYY-MM-DD");
const startTime = dayjs().startOf("month").format("YYYY-MM-DD");
return fetch(
"https://devops.aliyun.com/metric/api/card/work-time/distribution-detail",
{
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
projectIds: "",
userIds: userId,
startTime,
endTime,
tab: "time",
pluginSourceProject: "projex",
pluginType: "workTime",
templateId: "",
projectGroupIds: null,
toPage: 1,
pageSize: 40,
keyWord: "",
order: "desc",
showCopyAndDownloadButton: false,
sort: "",
timeUsage: "arranged",
types: "1,2,3",
groupColumns: "default"
}),
method: "POST",
mode: "cors",
credentials: "include"
}
).then((res) => res.json()).then((res) => {
return res.result.content.map((e) => ({
...e,
dateStr: dayjs(e.date).format("YYYY-MM-DD")
}));
});
}
async function updateWorkingHours(userId) {
const list = await getWorkingHours(userId);
let div = document.querySelector(`.${workClassName}`);
if (!div) {
div = document.createElement("div");
div.classList.add(workClassName);
div.addEventListener("click", async () => {
if (isCheat) {
const userName = window.prompt("请输入用户");
const allMembers = await getAllMembers();
const userId2 = allMembers.find((e) => e.name === userName)?._userId;
await updateWorkingHours(userId2);
} else {
await updateWorkingHours();
}
});
document.querySelector(".system-bar-middle")?.appendChild(div);
}
const todayWorkHours = list[list.length - 1]?.actualWorkTime || 0;
const monthWorkHours = list.reduce((acc, cur) => acc + cur.actualWorkTime, 0);
div.classList.add(workClassName);
div.textContent = `[有几分钟延迟]今天工时: ${todayWorkHours} 小时, 这个月已累计: ${monthWorkHours.toFixed(2)} 小时(${(monthWorkHours / 4).toFixed(2)}个任务)`;
}
const init = () => {
rxjs.timer(100).subscribe(() => {
updateWorkingHours();
});
};
const match = (path) => {
return (
// 工作项视图
/^\/projex\/workitem/.test(path) || // 工作台视图
/^\/projex\/project/.test(path)
);
};
const apiMaps = /* @__PURE__ */ new Map([
[
"/projex/api/workitem/workitem/time",
() => {
rxjs.timer(5 * 60 * 1e3).subscribe(() => {
updateWorkingHours();
});
}
]
]);
const name = "工时统计";
const workingHours = {
match,
apiMaps,
name,
init
};
const chain = new ChainOfResponsibility();
chain.add(taskHandler).add(workingHours);
const xhrOpen = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function(...args) {
xhrOpen.apply(this, args);
let triggerURLPath = location.pathname;
this.addEventListener("readystatechange", function() {
if (this.readyState === 4 && this.status === 200) {
const path = new URL(this.responseURL).pathname;
const responseJSON = this.responseType === "json" ? this.response : JSON.parse(this.responseText);
chain.handleApi({
path,
responseJSON,
triggerURLPath
});
}
});
};
initKeyBind();
})(rxjs, dayjs);