MonkeyModifier

Change webpage content

当前为 2024-08-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MonkeyModifier
  3. // @namespace https://github.com/JiyuShao/greasyfork-scripts
  4. // @version 2024-08-14
  5. // @description Change webpage content
  6. // @author Jiyu Shao <jiyu.shao@gmail.com>
  7. // @license MIT
  8. // @match *://*/*
  9. // @run-at document-start
  10. // @grant unsafeWindow
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15. // ################### common tools
  16. function replaceTextInNode(node, originalText, replaceText) {
  17. // 如果当前节点是文本节点并且包含 originalText
  18. if (node instanceof Text && node.textContent.includes(originalText)) {
  19. // 替换文本
  20. node.textContent = node.textContent.replace(originalText, replaceText);
  21. }
  22.  
  23. // 如果当前节点有子节点,递归处理每个子节点
  24. if (node.hasChildNodes()) {
  25. node.childNodes.forEach((child) => {
  26. replaceTextInNode(child, originalText, replaceText);
  27. });
  28. }
  29. }
  30.  
  31. function registerMutationObserver(node, config = {}, options = {}) {
  32. const finalConfig = {
  33. attributes: false,
  34. childList: true,
  35. subtree: true,
  36. ...config,
  37. };
  38.  
  39. const finalOptions = {
  40. // 元素的属性发生了变化
  41. attributes: options.attributes || [],
  42. // 子节点列表发生了变化
  43. childList: {
  44. addedNodes:
  45. options.childList.addedNodes ||
  46. [
  47. // {
  48. // filter: (node) => {},
  49. // action: (node) => {},
  50. // }
  51. ],
  52. removedNodes: options.childList.removedNodes || [],
  53. },
  54. // 文本节点的内容发生了变化
  55. characterData: options.characterData || [],
  56. };
  57.  
  58. const observer = new MutationObserver((mutationsList, _observer) => {
  59. mutationsList.forEach((mutation) => {
  60. if (mutation.type === 'attributes') {
  61. finalOptions.attributes.forEach(({ filter, action }) => {
  62. try {
  63. if (filter(mutation.target, mutation)) {
  64. action(mutation.target, mutation);
  65. }
  66. } catch (error) {
  67. console.error(
  68. 'MutationObserver attributes callback failed:',
  69. mutation.target,
  70. error
  71. );
  72. }
  73. });
  74. }
  75. if (mutation.type === 'childList') {
  76. // 检查是否有新增的元素
  77. mutation.addedNodes.forEach((node) => {
  78. finalOptions.childList.addedNodes.forEach(({ filter, action }) => {
  79. try {
  80. if (filter(node, mutation)) {
  81. action(node, mutation);
  82. }
  83. } catch (error) {
  84. console.error(
  85. 'MutationObserver childList.addedNodes callback failed:',
  86. node,
  87. error
  88. );
  89. }
  90. });
  91. });
  92.  
  93. // 检查是否有删除元素
  94. mutation.removedNodes.forEach((node) => {
  95. finalOptions.childList.removedNodes.forEach((filter, action) => {
  96. try {
  97. if (filter(node, mutation)) {
  98. action(node, mutation);
  99. }
  100. } catch (error) {
  101. console.error(
  102. 'MutationObserver childList.removedNodes callback failed:',
  103. node,
  104. error
  105. );
  106. }
  107. });
  108. });
  109. }
  110. if (mutation.type === 'characterData') {
  111. finalOptions.characterData.forEach(({ filter, action }) => {
  112. try {
  113. if (filter(mutation.target, mutation)) {
  114. action(mutation.target, mutation);
  115. }
  116. } catch (error) {
  117. console.error(
  118. 'MutationObserver characterData callback failed:',
  119. mutation.target,
  120. error
  121. );
  122. }
  123. });
  124. }
  125. });
  126. });
  127. observer.observe(node, finalConfig);
  128. return observer;
  129. }
  130.  
  131. function registerFetchModifier(modifierList) {
  132. const originalFetch = unsafeWindow.fetch;
  133. unsafeWindow.fetch = function (url, options) {
  134. let finalUrl = url;
  135. let finalOptions = { ...options };
  136. let finalResult = null;
  137. const matchedModifierList = modifierList.filter((e) =>
  138. e.test(finalUrl, finalOptions)
  139. );
  140. for (const currentModifier of matchedModifierList) {
  141. if (currentModifier.prerequest) {
  142. [finalUrl, finalOptions] = currentModifier.prerequest(
  143. finalUrl,
  144. finalOptions
  145. );
  146. }
  147. }
  148. finalResult = originalFetch(finalUrl, finalOptions);
  149. for (const currentModifier of matchedModifierList) {
  150. if (currentModifier.preresponse) {
  151. finalResult = currentModifier.preresponse(finalResult);
  152. }
  153. }
  154. return finalResult;
  155. };
  156. }
  157.  
  158. function registerXMLHttpRequestPolyfill() {
  159. // 保存原始的 XMLHttpRequest 构造函数
  160. const originalXMLHttpRequest = unsafeWindow.XMLHttpRequest;
  161.  
  162. // 定义新的 XMLHttpRequest 构造函数
  163. unsafeWindow.XMLHttpRequest = class extends originalXMLHttpRequest {
  164. constructor() {
  165. super();
  166. this._responseType = ''; // 存储 responseType
  167. this._onreadystatechange = null; // 存储 onreadystatechange 函数
  168. this._onload = null; // 存储 onload 函数
  169. this._sendData = null; // 存储 send 方法的数据
  170. this._headers = {}; // 存储请求头
  171. this._method = null; // 存储请求方法
  172. this._url = null; // 存储请求 URL
  173. this._async = true; // 存储异步标志
  174. this._user = null; // 存储用户名
  175. this._password = null; // 存储密码
  176. this._readyState = XMLHttpRequest.UNSENT; // 存储 readyState
  177. this._status = 0; // 存储状态码
  178. this._statusText = ''; // 存储状态文本
  179. this._response = null; // 存储响应对象
  180. this._responseText = ''; // 存储响应文本
  181. }
  182.  
  183. open(method, url, async = true, user = null, password = null) {
  184. this._method = method;
  185. this._url = url;
  186. this._async = async;
  187. this._user = user;
  188. this._password = password;
  189. this._readyState = XMLHttpRequest.OPENED;
  190. }
  191.  
  192. send(data) {
  193. this._sendData = data;
  194. this._sendRequest();
  195. }
  196.  
  197. _sendRequest() {
  198. const self = this;
  199.  
  200. // 根据 responseType 设置 fetch 的返回类型
  201. let fetchOptions = {
  202. method: this._method,
  203. headers: new Headers(this._headers),
  204. };
  205.  
  206. // 设置请求体
  207. if (this._sendData !== null) {
  208. fetchOptions.body = this._sendData;
  209. }
  210.  
  211. if (this.withCredentials) {
  212. fetchOptions.credentials = 'include';
  213. }
  214.  
  215. // 发送 fetch 请求
  216. unsafeWindow
  217. .fetch(this._url, fetchOptions)
  218. .then((response) => {
  219. self._response = response;
  220. self._status = response.status;
  221. self._statusText = response.statusText;
  222. self._readyState = XMLHttpRequest.DONE;
  223. const responseType = self._responseType || 'text';
  224. // 设置响应类型
  225. switch (responseType) {
  226. case 'json':
  227. return response.json().then((json) => {
  228. self._responseText = JSON.stringify(json);
  229. self._response = json;
  230. self._onreadystatechange && self._onreadystatechange();
  231. self._onload && self._onload();
  232. });
  233. case 'text':
  234. return response.text().then((text) => {
  235. self._responseText = text;
  236. self._response = text;
  237. self._onreadystatechange && self._onreadystatechange();
  238. self._onload && self._onload();
  239. });
  240. case 'blob':
  241. return response.blob().then((blob) => {
  242. self._response = blob;
  243. self._onreadystatechange && self._onreadystatechange();
  244. self._onload && self._onload();
  245. });
  246. }
  247. })
  248. .catch((error) => {
  249. self._readyState = XMLHttpRequest.DONE;
  250. self._status = 0;
  251. self._statusText = 'Network Error';
  252. self._onreadystatechange && self._onreadystatechange();
  253. self._onload && self._onload();
  254. });
  255. }
  256.  
  257. setRequestHeader(name, value) {
  258. this._headers[name] = value;
  259. return this;
  260. }
  261.  
  262. getResponseHeader(name) {
  263. return this._response && this._response.headers
  264. ? this._response.headers.get(name)
  265. : null;
  266. }
  267.  
  268. getAllResponseHeaders() {
  269. return this._response && this._response.headers
  270. ? this._response.headers
  271. : null;
  272. }
  273.  
  274. set onreadystatechange(callback) {
  275. this._onreadystatechange = callback;
  276. }
  277.  
  278. set onload(callback) {
  279. this._onload = callback;
  280. }
  281.  
  282. get readyState() {
  283. return this._readyState;
  284. }
  285.  
  286. set readyState(state) {
  287. this._readyState = state;
  288. }
  289.  
  290. get response() {
  291. return this._response;
  292. }
  293.  
  294. set response(value) {
  295. this._response = value;
  296. }
  297.  
  298. get responseText() {
  299. return this._responseText;
  300. }
  301.  
  302. set responseText(value) {
  303. this._responseText = value;
  304. }
  305.  
  306. get status() {
  307. return this._status;
  308. }
  309.  
  310. set status(value) {
  311. this._status = value;
  312. }
  313.  
  314. get statusText() {
  315. return this._statusText;
  316. }
  317.  
  318. set statusText(value) {
  319. this._statusText = value;
  320. }
  321.  
  322. get responseType() {
  323. return this._responseType;
  324. }
  325.  
  326. set responseType(type) {
  327. this._responseType = type;
  328. }
  329. };
  330. }
  331.  
  332. function downloadCSV(arrayOfData, filename) {
  333. // 处理数据,使其适合 CSV 格式
  334. const csvContent = arrayOfData
  335. .map((row) =>
  336. row.map((cell) => `"${(cell || '').replace(/"/g, '""')}"`).join(',')
  337. )
  338. .join('\n');
  339.  
  340. // 在 CSV 内容前加上 BOM
  341. const bom = '\uFEFF';
  342. const csvContentWithBOM = bom + csvContent;
  343.  
  344. // 将内容转换为 Blob
  345. const blob = new Blob([csvContentWithBOM], {
  346. type: 'text/csv;charset=utf-8;',
  347. });
  348.  
  349. // 创建一个隐藏的可下载链接
  350. const url = URL.createObjectURL(blob);
  351. const link = document.createElement('a');
  352. link.href = url;
  353. link.setAttribute('download', `${filename}.csv`); // 指定文件名
  354. document.body.appendChild(link);
  355. link.click(); // 触发点击事件
  356. document.body.removeChild(link); // 清除链接
  357. URL.revokeObjectURL(url); // 释放 URL 对象
  358. }
  359. // ################### 加载前插入样式覆盖
  360. const style = document.createElement('style');
  361. const cssRules = `
  362. .dropdown-submenu--viewmode {
  363. display: none !important;
  364. }
  365. [field=modified] {
  366. display: none !important;
  367. }
  368.  
  369. [data-value=modified] {
  370. display: none !important;
  371. }
  372. [data-value=lastmodify] {
  373. display: none !important;
  374. }
  375.  
  376. [data-grid-field=modified] {
  377. display: none !important;
  378. }
  379.  
  380. [data-field-key=modified] {
  381. display: none !important;
  382. }
  383.  
  384. #Revisions {
  385. display: none !important;
  386. }
  387.  
  388. #ContentModified {
  389. display: none !important;
  390. }
  391.  
  392. [title="最后修改时间"] {
  393. display: none !important;
  394. }
  395.  
  396. .left-tree-bottom__manager-company--wide {
  397. display: none !important;
  398. }
  399.  
  400. .left-tree-narrow .left-tree-bottom__personal--icons > a:nth-child(1) {
  401. display: none !important;
  402. }
  403. `;
  404. style.appendChild(document.createTextNode(cssRules));
  405. unsafeWindow.document.head.appendChild(style);
  406.  
  407. // ################### 网页内容加载完成立即执行脚本
  408. unsafeWindow.addEventListener('DOMContentLoaded', function () {
  409. // 监听任务右侧基本信息
  410. const taskRightInfoEles =
  411. unsafeWindow.document.querySelectorAll('#ContentModified');
  412. taskRightInfoEles.forEach((element) => {
  413. const parentDiv = element.closest('div.left_3_col');
  414. if (parentDiv) {
  415. parentDiv.style.display = 'none';
  416. }
  417. });
  418. });
  419.  
  420. // ################### 加载完成动态监听
  421. unsafeWindow.addEventListener('load', function () {
  422. registerMutationObserver(
  423. unsafeWindow.document.body,
  424. {
  425. attributes: false,
  426. childList: true,
  427. subtree: true,
  428. },
  429. {
  430. childList: {
  431. addedNodes: [
  432. // 动态文本替换问题
  433. {
  434. filter: (node, _mutation) => {
  435. return node.textContent.includes('最后修改时间');
  436. },
  437. action: (node, _mutation) => {
  438. replaceTextInNode(node, '最后修改时间', '迭代修改时间');
  439. },
  440. },
  441. // 监听动态弹窗 隐藏设置列表字段-最后修改时间左侧
  442. {
  443. filter: (node, _mutation) => {
  444. return (
  445. node.querySelectorAll &&
  446. node.querySelectorAll('input[value=modified]').length > 0
  447. );
  448. },
  449. action: (node, _mutation) => {
  450. node
  451. .querySelectorAll('input[value=modified]')
  452. .forEach((ele) => {
  453. const parentDiv = ele.closest('div.field');
  454. if (parentDiv) {
  455. parentDiv.style.display = 'none';
  456. }
  457. });
  458. },
  459. },
  460. // 监听动态弹窗 隐藏设置列表字段-最后修改时间右侧
  461. {
  462. filter: (node, _mutation) => {
  463. return (
  464. node.querySelectorAll &&
  465. node.querySelectorAll('span[title=最后修改时间]').length > 0
  466. );
  467. },
  468. action: (node, _mutation) => {
  469. node
  470. .querySelectorAll('span[title=最后修改时间]')
  471. .forEach((ele) => {
  472. const parentDiv = ele.closest('div[role=treeitem]');
  473. if (parentDiv) {
  474. parentDiv.style.display = 'none';
  475. }
  476. });
  477. },
  478. },
  479. // 监听企业微信导出按钮
  480. {
  481. filter: (node, _mutation) => {
  482. return (
  483. node.querySelectorAll &&
  484. node.querySelectorAll('.js_export').length > 0
  485. );
  486. },
  487. action: (node, _mutation) => {
  488. function convertTimestampToTime(timestamp) {
  489. // 创建 Date 对象
  490. const date = new Date(timestamp * 1000); // Unix 时间戳是以秒为单位,而 Date 需要毫秒
  491.  
  492. // 获取小时和分钟
  493. const hours = date.getHours();
  494. const minutes = date.getMinutes();
  495.  
  496. // 确定上午还是下午
  497. const amPm = hours >= 12 ? '下午' : '上午';
  498.  
  499. // 返回格式化的字符串
  500. return `${amPm}${hours}:${minutes
  501. .toString()
  502. .padStart(2, '0')}`;
  503. }
  504. node.querySelectorAll('.js_export').forEach((ele) => {
  505. ele.addEventListener('click', async function (event) {
  506. event.preventDefault();
  507. event.stopPropagation();
  508. const response = await unsafeWindow.fetch(
  509. '/wework_admin/getAdminOperationRecord?lang=zh_CN&f=json&ajax=1&timeZoneInfo%5Bzone_offset%5D=-8',
  510. {
  511. headers: {
  512. 'content-type': 'application/x-www-form-urlencoded',
  513. },
  514. body: unsafeWindow.fetchTmpBody,
  515. method: 'POST',
  516. mode: 'cors',
  517. credentials: 'include',
  518. }
  519. );
  520. const responseJson = await response.json();
  521. const excelData = responseJson.data.operloglist.reduce(
  522. (result, current) => {
  523. const typeMapping = {
  524. 9: '新增部门',
  525. 10: '删除部门',
  526. 11: '移动部门',
  527. 13: '删除成员',
  528. 14: '新增成员',
  529. 15: '更改成员信息',
  530. 21: '更改部门信息',
  531. 23: '登录后台',
  532. 25: '发送邀请',
  533. 36: '修改管理组管理员列表',
  534. 35: '修改管理组应用权限',
  535. 34: '修改管理组通讯录权限',
  536. 88: '修改汇报规则',
  537. 120: '导出相关操作记录',
  538. 162: '批量设置成员信息',
  539. };
  540. const optTypeArray = {
  541. 0: '全部',
  542. 3: '成员与部门变更',
  543. 2: '权限管理变更',
  544. 12: '企业信息管理',
  545. 11: '通讯录与聊天管理',
  546. 13: '外部联系人管理',
  547. 8: '应用变更',
  548. 7: '其他',
  549. };
  550. return [
  551. ...result,
  552. [
  553. convertTimestampToTime(current.operatetime),
  554. current.op_name,
  555. optTypeArray[current.type_oper_1],
  556. typeMapping[current.type] || '其他',
  557. current.data,
  558. current.ip,
  559. ],
  560. ];
  561. },
  562. [
  563. [
  564. '时间',
  565. '操作者',
  566. '操作类型',
  567. '操作行为',
  568. '相关数据',
  569. '操作者IP',
  570. ],
  571. ]
  572. );
  573. downloadCSV(excelData, '管理端操作记录');
  574. });
  575. });
  576. },
  577. },
  578. // 监听钉钉审计日志
  579. {
  580. filter: (node, _mutation) => {
  581. return (
  582. node.querySelectorAll &&
  583. Array.from(
  584. node.querySelectorAll(
  585. '.audit-content tbody tr>td:nth-child(4)>div'
  586. )
  587. ).filter((e) =>
  588. ['删除部门', '添加部门', '部门名称修改'].includes(
  589. e.innerText
  590. )
  591. ).length > 0
  592. );
  593. },
  594. action: (node, _mutation) => {
  595. Array.from(
  596. node.querySelectorAll(
  597. '.audit-content tbody tr>td:nth-child(4)>div'
  598. )
  599. )
  600. .filter((e) =>
  601. ['删除部门', '添加部门', '部门名称修改'].includes(
  602. e.innerText
  603. )
  604. )
  605. .forEach((ele) => {
  606. const parentDiv = ele.closest('tr.dtd-table-row');
  607. if (parentDiv) {
  608. parentDiv.style.display = 'none';
  609. }
  610. });
  611. },
  612. },
  613. // 监听钉钉审计日志-导出按钮
  614. {
  615. filter: (node, _mutation) => {
  616. return (
  617. node.querySelectorAll &&
  618. Array.from(
  619. node.querySelectorAll(
  620. '.audit-content .dd-toolbar-btns-container .dd-toolbar-action-btns > div:nth-child(2)'
  621. )
  622. ).length > 0
  623. );
  624. },
  625. action: (node, _mutation) => {
  626. node
  627. .querySelectorAll(
  628. '.audit-content .dd-toolbar-btns-container .dd-toolbar-action-btns > div:nth-child(2)'
  629. )
  630. .forEach((ele) => {
  631. if (ele) {
  632. ele.style.display = 'none';
  633. }
  634. });
  635. },
  636. },
  637. ],
  638. },
  639. }
  640. );
  641. });
  642.  
  643. // ################### 替换请求
  644. if (
  645. unsafeWindow.location.pathname.startsWith('/wework_admin') &&
  646. !unsafeWindow.location.href.includes('loginpage_wx')
  647. ) {
  648. registerFetchModifier([
  649. {
  650. test: (url, options) => {
  651. return url.includes('/wework_admin/getAdminOperationRecord');
  652. },
  653. prerequest: (url, options) => {
  654. options.body = options.body
  655. .split('&')
  656. .reduce((result, current) => {
  657. let [key, value] = current.split('=');
  658. if (key === 'limit') {
  659. value = 500;
  660. }
  661. return [...result, `${key}=${value}`];
  662. }, [])
  663. .join('&');
  664. unsafeWindow.fetchTmpBody = options.body;
  665. return [url, options];
  666. },
  667. preresponse: async (responsePromise) => {
  668. const response = await responsePromise;
  669. let responseJson = await response.json();
  670. responseJson.data.operloglist = responseJson.data.operloglist.filter(
  671. (e) => e.type_oper_1 !== 3
  672. );
  673. responseJson.data.total = responseJson.data.operloglist.length;
  674. return new Response(JSON.stringify(responseJson), {
  675. headers: response.headers,
  676. ok: response.ok,
  677. redirected: response.redirected,
  678. status: response.status,
  679. statusText: response.statusText,
  680. type: response.type,
  681. url: response.url,
  682. });
  683. },
  684. },
  685. ]);
  686. registerXMLHttpRequestPolyfill();
  687. }
  688. })();