MonkeyModifier

Change webpage content

Tính đến 12-08-2024. Xem phiên bản mới nhất.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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;
    };

    // 保存原始的 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.get(name) : null;
      }

      getAllResponseHeaders() {
        return this._response ? 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, '管理端操作记录');
                  });
                });
              },
            },
          ],
        },
      }
    );
  });

  // ################### 替换请求
  if (
    unsafeWindow.location.pathname.startsWith('/wework_admin') &&
    !unsafeWindow.location.pathname.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,
          });
        },
      },
    ]);
  }
})();