// ==UserScript==
// @name MonkeyModifier
// @namespace https://github.com/JiyuShao/greasyfork-scripts
// @version 2024-08-12
// @description Change webpage content
// @author Jiyu Shao <[email protected]>
// @license MIT
// @match *://*/*
// @run-at document-start
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
// ################### common tools
function replaceTextInNode(node, originalText, replaceText) {
// 如果当前节点是文本节点并且包含 originalText
if (node instanceof Text && node.textContent.includes(originalText)) {
// 替换文本
node.textContent = node.textContent.replace(originalText, replaceText);
}
// 如果当前节点有子节点,递归处理每个子节点
if (node.hasChildNodes()) {
node.childNodes.forEach((child) => {
replaceTextInNode(child, originalText, replaceText);
});
}
}
function registerMutationObserver(node, config = {}, options = {}) {
const finalConfig = {
attributes: false,
childList: true,
subtree: true,
...config,
};
const finalOptions = {
// 元素的属性发生了变化
attributes: options.attributes || [],
// 子节点列表发生了变化
childList: {
addedNodes:
options.childList.addedNodes ||
[
// {
// filter: (node) => {},
// action: (node) => {},
// }
],
removedNodes: options.childList.removedNodes || [],
},
// 文本节点的内容发生了变化
characterData: options.characterData || [],
};
const observer = new MutationObserver((mutationsList, _observer) => {
mutationsList.forEach((mutation) => {
if (mutation.type === 'attributes') {
finalOptions.attributes.forEach(({ filter, action }) => {
try {
if (filter(mutation.target, mutation)) {
action(mutation.target, mutation);
}
} catch (error) {
console.error(
'MutationObserver attributes callback failed:',
mutation.target,
error
);
}
});
}
if (mutation.type === 'childList') {
// 检查是否有新增的元素
mutation.addedNodes.forEach((node) => {
finalOptions.childList.addedNodes.forEach(({ filter, action }) => {
try {
if (filter(node, mutation)) {
action(node, mutation);
}
} catch (error) {
console.error(
'MutationObserver childList.addedNodes callback failed:',
node,
error
);
}
});
});
// 检查是否有删除元素
mutation.removedNodes.forEach((node) => {
finalOptions.childList.removedNodes.forEach((filter, action) => {
try {
if (filter(node, mutation)) {
action(node, mutation);
}
} catch (error) {
console.error(
'MutationObserver childList.removedNodes callback failed:',
node,
error
);
}
});
});
}
if (mutation.type === 'characterData') {
finalOptions.characterData.forEach(({ filter, action }) => {
try {
if (filter(mutation.target, mutation)) {
action(mutation.target, mutation);
}
} catch (error) {
console.error(
'MutationObserver characterData callback failed:',
mutation.target,
error
);
}
});
}
});
});
observer.observe(node, finalConfig);
return observer;
}
function registerFetchModifier(modifierList) {
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = function (url, options) {
let finalUrl = url;
let finalOptions = { ...options };
let finalResult = null;
const matchedModifierList = modifierList.filter((e) =>
e.test(finalUrl, finalOptions)
);
for (const currentModifier of matchedModifierList) {
if (currentModifier.prerequest) {
[finalUrl, finalOptions] = currentModifier.prerequest(
finalUrl,
finalOptions
);
}
}
finalResult = originalFetch(finalUrl, finalOptions);
for (const currentModifier of matchedModifierList) {
if (currentModifier.preresponse) {
finalResult = currentModifier.preresponse(finalResult);
}
}
return finalResult;
};
}
function registerXMLHttpRequestPolyfill() {
// 保存原始的 XMLHttpRequest 构造函数
const originalXMLHttpRequest = unsafeWindow.XMLHttpRequest;
// 定义新的 XMLHttpRequest 构造函数
unsafeWindow.XMLHttpRequest = class extends originalXMLHttpRequest {
constructor() {
super();
this._responseType = 'text'; // 存储 responseType
this._onreadystatechange = null; // 存储 onreadystatechange 函数
this._onload = null; // 存储 onload 函数
this._sendData = null; // 存储 send 方法的数据
this._headers = {}; // 存储请求头
this._method = null; // 存储请求方法
this._url = null; // 存储请求 URL
this._async = true; // 存储异步标志
this._user = null; // 存储用户名
this._password = null; // 存储密码
this._readyState = XMLHttpRequest.UNSENT; // 存储 readyState
this._status = 0; // 存储状态码
this._statusText = ''; // 存储状态文本
this._response = null; // 存储响应对象
this._responseText = ''; // 存储响应文本
}
open(method, url, async = true, user = null, password = null) {
this._method = method;
this._url = url;
this._async = async;
this._user = user;
this._password = password;
this._readyState = XMLHttpRequest.OPENED;
}
send(data) {
this._sendData = data;
this._sendRequest();
}
_sendRequest() {
const self = this;
// 根据 responseType 设置 fetch 的返回类型
let fetchOptions = {
method: this._method,
headers: new Headers(),
};
// 设置请求体
if (this._sendData !== null) {
fetchOptions.body = this._sendData;
}
// 设置请求头
if (this._headers) {
Object.keys(this._headers).forEach((header) => {
fetchOptions.headers.set(header, this._headers[header]);
});
}
// 发送 fetch 请求
unsafeWindow
.fetch(this._url, fetchOptions)
.then((response) => {
self._response = response;
self._status = response.status;
self._statusText = response.statusText;
self._readyState = XMLHttpRequest.DONE;
// 设置响应类型
switch (self._responseType) {
case 'json':
return response.json().then((json) => {
self._responseText = JSON.stringify(json);
self._response = json;
self._onreadystatechange && self._onreadystatechange();
self._onload && self._onload();
});
case 'text':
return response.text().then((text) => {
self._responseText = text;
self._response = text;
self._onreadystatechange && self._onreadystatechange();
self._onload && self._onload();
});
case 'blob':
return response.blob().then((blob) => {
self._response = blob;
self._onreadystatechange && self._onreadystatechange();
self._onload && self._onload();
});
}
})
.catch((error) => {
self._readyState = XMLHttpRequest.DONE;
self._status = 0;
self._statusText = 'Network Error';
self._onreadystatechange && self._onreadystatechange();
self._onload && self._onload();
});
}
setRequestHeader(name, value) {
this._headers[name] = value;
return this;
}
getResponseHeader(name) {
return this._response && this._response.headers
? this._response.headers.get(name)
: null;
}
getAllResponseHeaders() {
return this._response && this._response.headers
? this._response.headers
: null;
}
set onreadystatechange(callback) {
this._onreadystatechange = callback;
}
set onload(callback) {
this._onload = callback;
}
get readyState() {
return this._readyState;
}
set readyState(state) {
this._readyState = state;
}
get response() {
return this._response;
}
set response(value) {
this._response = value;
}
get responseText() {
return this._responseText;
}
set responseText(value) {
this._responseText = value;
}
get status() {
return this._status;
}
set status(value) {
this._status = value;
}
get statusText() {
return this._statusText;
}
set statusText(value) {
this._statusText = value;
}
get responseType() {
return this._responseType;
}
set responseType(type) {
this._responseType = type;
}
};
}
function downloadCSV(arrayOfData, filename) {
// 处理数据,使其适合 CSV 格式
const csvContent = arrayOfData
.map((row) =>
row.map((cell) => `"${(cell || '').replace(/"/g, '""')}"`).join(',')
)
.join('\n');
// 在 CSV 内容前加上 BOM
const bom = '\uFEFF';
const csvContentWithBOM = bom + csvContent;
// 将内容转换为 Blob
const blob = new Blob([csvContentWithBOM], {
type: 'text/csv;charset=utf-8;',
});
// 创建一个隐藏的可下载链接
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${filename}.csv`); // 指定文件名
document.body.appendChild(link);
link.click(); // 触发点击事件
document.body.removeChild(link); // 清除链接
URL.revokeObjectURL(url); // 释放 URL 对象
}
// ################### 加载前插入样式覆盖
const style = document.createElement('style');
const cssRules = `
.dropdown-submenu--viewmode {
display: none !important;
}
[field=modified] {
display: none !important;
}
[data-value=modified] {
display: none !important;
}
[data-value=lastmodify] {
display: none !important;
}
[data-grid-field=modified] {
display: none !important;
}
[data-field-key=modified] {
display: none !important;
}
#Revisions {
display: none !important;
}
#ContentModified {
display: none !important;
}
[title="最后修改时间"] {
display: none !important;
}
.left-tree-bottom__manager-company--wide {
display: none !important;
}
.left-tree-narrow .left-tree-bottom__personal--icons > a:nth-child(1) {
display: none !important;
}
`;
style.appendChild(document.createTextNode(cssRules));
unsafeWindow.document.head.appendChild(style);
// ################### 网页内容加载完成立即执行脚本
unsafeWindow.addEventListener('DOMContentLoaded', function () {
// 监听任务右侧基本信息
const taskRightInfoEles =
unsafeWindow.document.querySelectorAll('#ContentModified');
taskRightInfoEles.forEach((element) => {
const parentDiv = element.closest('div.left_3_col');
if (parentDiv) {
parentDiv.style.display = 'none';
}
});
});
// ################### 加载完成动态监听
unsafeWindow.addEventListener('load', function () {
registerMutationObserver(
unsafeWindow.document.body,
{
attributes: false,
childList: true,
subtree: true,
},
{
childList: {
addedNodes: [
// 动态文本替换问题
{
filter: (node, _mutation) => {
return node.textContent.includes('最后修改时间');
},
action: (node, _mutation) => {
replaceTextInNode(node, '最后修改时间', '迭代修改时间');
},
},
// 监听动态弹窗 隐藏设置列表字段-最后修改时间左侧
{
filter: (node, _mutation) => {
return (
node.querySelectorAll &&
node.querySelectorAll('input[value=modified]').length > 0
);
},
action: (node, _mutation) => {
node
.querySelectorAll('input[value=modified]')
.forEach((ele) => {
const parentDiv = ele.closest('div.field');
if (parentDiv) {
parentDiv.style.display = 'none';
}
});
},
},
// 监听动态弹窗 隐藏设置列表字段-最后修改时间右侧
{
filter: (node, _mutation) => {
return (
node.querySelectorAll &&
node.querySelectorAll('span[title=最后修改时间]').length > 0
);
},
action: (node, _mutation) => {
node
.querySelectorAll('span[title=最后修改时间]')
.forEach((ele) => {
const parentDiv = ele.closest('div[role=treeitem]');
if (parentDiv) {
parentDiv.style.display = 'none';
}
});
},
},
// 监听企业微信导出按钮
{
filter: (node, _mutation) => {
return (
node.querySelectorAll &&
node.querySelectorAll('.js_export').length > 0
);
},
action: (node, _mutation) => {
function convertTimestampToTime(timestamp) {
// 创建 Date 对象
const date = new Date(timestamp * 1000); // Unix 时间戳是以秒为单位,而 Date 需要毫秒
// 获取小时和分钟
const hours = date.getHours();
const minutes = date.getMinutes();
// 确定上午还是下午
const amPm = hours >= 12 ? '下午' : '上午';
// 返回格式化的字符串
return `${amPm}${hours}:${minutes
.toString()
.padStart(2, '0')}`;
}
node.querySelectorAll('.js_export').forEach((ele) => {
ele.addEventListener('click', async function (event) {
event.preventDefault();
event.stopPropagation();
const response = await unsafeWindow.fetch(
'/wework_admin/getAdminOperationRecord?lang=zh_CN&f=json&ajax=1&timeZoneInfo%5Bzone_offset%5D=-8',
{
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: unsafeWindow.fetchTmpBody,
method: 'POST',
mode: 'cors',
credentials: 'include',
}
);
const responseJson = await response.json();
const excelData = responseJson.data.operloglist.reduce(
(result, current) => {
const typeMapping = {
9: '新增部门',
10: '删除部门',
11: '移动部门',
13: '删除成员',
14: '新增成员',
15: '更改成员信息',
21: '更改部门信息',
23: '登录(不可用)后台',
25: '发送邀请',
36: '修改管理组管理员列表',
35: '修改管理组应用权限',
34: '修改管理组通讯录权限',
88: '修改汇报规则',
120: '导出相关操作记录',
162: '批量设置成员信息',
};
const optTypeArray = {
0: '全部',
3: '成员与部门变更',
2: '权限管理变更',
12: '企业信息管理',
11: '通讯录与聊天管理',
13: '外部联系人管理',
8: '应用变更',
7: '其他',
};
return [
...result,
[
convertTimestampToTime(current.operatetime),
current.op_name,
optTypeArray[current.type_oper_1],
typeMapping[current.type] || '其他',
current.data,
current.ip,
],
];
},
[
[
'时间',
'操作者',
'操作类型',
'操作行为',
'相关数据',
'操作者IP',
],
]
);
downloadCSV(excelData, '管理端操作记录');
});
});
},
},
// 监听钉钉审计日志
{
filter: (node, _mutation) => {
return (
node.querySelectorAll &&
Array.from(
node.querySelectorAll(
'.audit-content tbody tr>td:nth-child(4)>div'
)
).filter((e) =>
['删除部门', '添加部门'].includes(e.innerText)
).length > 0
);
},
action: (node, _mutation) => {
node
.querySelectorAll(
'.audit-content tbody tr>td:nth-child(4)>div'
)
.forEach((ele) => {
const parentDiv = ele.closest('tr.dtd-table-row');
if (parentDiv) {
parentDiv.style.display = 'none';
}
});
},
},
],
},
}
);
});
// ################### 替换请求
if (
unsafeWindow.location.pathname.startsWith('/wework_admin') &&
!unsafeWindow.location.href.includes('loginpage_wx')
) {
registerFetchModifier([
{
test: (url, options) => {
return url.includes('/wework_admin/getAdminOperationRecord');
},
prerequest: (url, options) => {
options.body = options.body
.split('&')
.reduce((result, current) => {
let [key, value] = current.split('=');
if (key === 'limit') {
value = 500;
}
return [...result, `${key}=${value}`];
}, [])
.join('&');
unsafeWindow.fetchTmpBody = options.body;
return [url, options];
},
preresponse: async (responsePromise) => {
const response = await responsePromise;
let responseJson = await response.json();
responseJson.data.operloglist = responseJson.data.operloglist.filter(
(e) => e.type_oper_1 !== 3
);
responseJson.data.total = responseJson.data.operloglist.length;
return new Response(JSON.stringify(responseJson), {
headers: response.headers,
ok: response.ok,
redirected: response.redirected,
status: response.status,
statusText: response.statusText,
type: response.type,
url: response.url,
});
},
},
]);
registerXMLHttpRequestPolyfill();
}
})();