MonkeyModifier

Change webpage content

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

  1. // ==UserScript==
  2. // @name MonkeyModifier
  3. // @namespace https://github.com/JiyuShao/greasyfork-scripts
  4. // @version 2024-08-12
  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 = 'text'; // 存储 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(),
  204. };
  205.  
  206. // 设置请求体
  207. if (this._sendData !== null) {
  208. fetchOptions.body = this._sendData;
  209. }
  210.  
  211. // 设置请求头
  212. if (this._headers) {
  213. Object.keys(this._headers).forEach((header) => {
  214. fetchOptions.headers.set(header, this._headers[header]);
  215. });
  216. }
  217.  
  218. // 发送 fetch 请求
  219. unsafeWindow
  220. .fetch(this._url, fetchOptions)
  221. .then((response) => {
  222. self._response = response;
  223. self._status = response.status;
  224. self._statusText = response.statusText;
  225. self._readyState = XMLHttpRequest.DONE;
  226.  
  227. // 设置响应类型
  228. switch (self._responseType) {
  229. case 'json':
  230. return response.json().then((json) => {
  231. self._responseText = JSON.stringify(json);
  232. self._response = json;
  233. self._onreadystatechange && self._onreadystatechange();
  234. self._onload && self._onload();
  235. });
  236. case 'text':
  237. return response.text().then((text) => {
  238. self._responseText = text;
  239. self._response = text;
  240. self._onreadystatechange && self._onreadystatechange();
  241. self._onload && self._onload();
  242. });
  243. case 'blob':
  244. return response.blob().then((blob) => {
  245. self._response = blob;
  246. self._onreadystatechange && self._onreadystatechange();
  247. self._onload && self._onload();
  248. });
  249. }
  250. })
  251. .catch((error) => {
  252. self._readyState = XMLHttpRequest.DONE;
  253. self._status = 0;
  254. self._statusText = 'Network Error';
  255. self._onreadystatechange && self._onreadystatechange();
  256. self._onload && self._onload();
  257. });
  258. }
  259.  
  260. setRequestHeader(name, value) {
  261. this._headers[name] = value;
  262. return this;
  263. }
  264.  
  265. getResponseHeader(name) {
  266. return this._response && this._response.headers
  267. ? this._response.headers.get(name)
  268. : null;
  269. }
  270.  
  271. getAllResponseHeaders() {
  272. return this._response && this._response.headers
  273. ? this._response.headers
  274. : null;
  275. }
  276.  
  277. set onreadystatechange(callback) {
  278. this._onreadystatechange = callback;
  279. }
  280.  
  281. set onload(callback) {
  282. this._onload = callback;
  283. }
  284.  
  285. get readyState() {
  286. return this._readyState;
  287. }
  288.  
  289. set readyState(state) {
  290. this._readyState = state;
  291. }
  292.  
  293. get response() {
  294. return this._response;
  295. }
  296.  
  297. set response(value) {
  298. this._response = value;
  299. }
  300.  
  301. get responseText() {
  302. return this._responseText;
  303. }
  304.  
  305. set responseText(value) {
  306. this._responseText = value;
  307. }
  308.  
  309. get status() {
  310. return this._status;
  311. }
  312.  
  313. set status(value) {
  314. this._status = value;
  315. }
  316.  
  317. get statusText() {
  318. return this._statusText;
  319. }
  320.  
  321. set statusText(value) {
  322. this._statusText = value;
  323. }
  324.  
  325. get responseType() {
  326. return this._responseType;
  327. }
  328.  
  329. set responseType(type) {
  330. this._responseType = type;
  331. }
  332. };
  333. }
  334.  
  335. function downloadCSV(arrayOfData, filename) {
  336. // 处理数据,使其适合 CSV 格式
  337. const csvContent = arrayOfData
  338. .map((row) =>
  339. row.map((cell) => `"${(cell || '').replace(/"/g, '""')}"`).join(',')
  340. )
  341. .join('\n');
  342.  
  343. // 在 CSV 内容前加上 BOM
  344. const bom = '\uFEFF';
  345. const csvContentWithBOM = bom + csvContent;
  346.  
  347. // 将内容转换为 Blob
  348. const blob = new Blob([csvContentWithBOM], {
  349. type: 'text/csv;charset=utf-8;',
  350. });
  351.  
  352. // 创建一个隐藏的可下载链接
  353. const url = URL.createObjectURL(blob);
  354. const link = document.createElement('a');
  355. link.href = url;
  356. link.setAttribute('download', `${filename}.csv`); // 指定文件名
  357. document.body.appendChild(link);
  358. link.click(); // 触发点击事件
  359. document.body.removeChild(link); // 清除链接
  360. URL.revokeObjectURL(url); // 释放 URL 对象
  361. }
  362. // ################### 加载前插入样式覆盖
  363. const style = document.createElement('style');
  364. const cssRules = `
  365. .dropdown-submenu--viewmode {
  366. display: none !important;
  367. }
  368. [field=modified] {
  369. display: none !important;
  370. }
  371.  
  372. [data-value=modified] {
  373. display: none !important;
  374. }
  375. [data-value=lastmodify] {
  376. display: none !important;
  377. }
  378.  
  379. [data-grid-field=modified] {
  380. display: none !important;
  381. }
  382.  
  383. [data-field-key=modified] {
  384. display: none !important;
  385. }
  386.  
  387. #Revisions {
  388. display: none !important;
  389. }
  390.  
  391. #ContentModified {
  392. display: none !important;
  393. }
  394.  
  395. [title="最后修改时间"] {
  396. display: none !important;
  397. }
  398.  
  399. .left-tree-bottom__manager-company--wide {
  400. display: none !important;
  401. }
  402.  
  403. .left-tree-narrow .left-tree-bottom__personal--icons > a:nth-child(1) {
  404. display: none !important;
  405. }
  406. `;
  407. style.appendChild(document.createTextNode(cssRules));
  408. unsafeWindow.document.head.appendChild(style);
  409.  
  410. // ################### 网页内容加载完成立即执行脚本
  411. unsafeWindow.addEventListener('DOMContentLoaded', function () {
  412. // 监听任务右侧基本信息
  413. const taskRightInfoEles =
  414. unsafeWindow.document.querySelectorAll('#ContentModified');
  415. taskRightInfoEles.forEach((element) => {
  416. const parentDiv = element.closest('div.left_3_col');
  417. if (parentDiv) {
  418. parentDiv.style.display = 'none';
  419. }
  420. });
  421. });
  422.  
  423. // ################### 加载完成动态监听
  424. unsafeWindow.addEventListener('load', function () {
  425. registerMutationObserver(
  426. unsafeWindow.document.body,
  427. {
  428. attributes: false,
  429. childList: true,
  430. subtree: true,
  431. },
  432. {
  433. childList: {
  434. addedNodes: [
  435. // 动态文本替换问题
  436. {
  437. filter: (node, _mutation) => {
  438. return node.textContent.includes('最后修改时间');
  439. },
  440. action: (node, _mutation) => {
  441. replaceTextInNode(node, '最后修改时间', '迭代修改时间');
  442. },
  443. },
  444. // 监听动态弹窗 隐藏设置列表字段-最后修改时间左侧
  445. {
  446. filter: (node, _mutation) => {
  447. return (
  448. node.querySelectorAll &&
  449. node.querySelectorAll('input[value=modified]').length > 0
  450. );
  451. },
  452. action: (node, _mutation) => {
  453. node
  454. .querySelectorAll('input[value=modified]')
  455. .forEach((ele) => {
  456. const parentDiv = ele.closest('div.field');
  457. if (parentDiv) {
  458. parentDiv.style.display = 'none';
  459. }
  460. });
  461. },
  462. },
  463. // 监听动态弹窗 隐藏设置列表字段-最后修改时间右侧
  464. {
  465. filter: (node, _mutation) => {
  466. return (
  467. node.querySelectorAll &&
  468. node.querySelectorAll('span[title=最后修改时间]').length > 0
  469. );
  470. },
  471. action: (node, _mutation) => {
  472. node
  473. .querySelectorAll('span[title=最后修改时间]')
  474. .forEach((ele) => {
  475. const parentDiv = ele.closest('div[role=treeitem]');
  476. if (parentDiv) {
  477. parentDiv.style.display = 'none';
  478. }
  479. });
  480. },
  481. },
  482. // 监听企业微信导出按钮
  483. {
  484. filter: (node, _mutation) => {
  485. return (
  486. node.querySelectorAll &&
  487. node.querySelectorAll('.js_export').length > 0
  488. );
  489. },
  490. action: (node, _mutation) => {
  491. function convertTimestampToTime(timestamp) {
  492. // 创建 Date 对象
  493. const date = new Date(timestamp * 1000); // Unix 时间戳是以秒为单位,而 Date 需要毫秒
  494.  
  495. // 获取小时和分钟
  496. const hours = date.getHours();
  497. const minutes = date.getMinutes();
  498.  
  499. // 确定上午还是下午
  500. const amPm = hours >= 12 ? '下午' : '上午';
  501.  
  502. // 返回格式化的字符串
  503. return `${amPm}${hours}:${minutes
  504. .toString()
  505. .padStart(2, '0')}`;
  506. }
  507. node.querySelectorAll('.js_export').forEach((ele) => {
  508. ele.addEventListener('click', async function (event) {
  509. event.preventDefault();
  510. event.stopPropagation();
  511. const response = await unsafeWindow.fetch(
  512. '/wework_admin/getAdminOperationRecord?lang=zh_CN&f=json&ajax=1&timeZoneInfo%5Bzone_offset%5D=-8',
  513. {
  514. headers: {
  515. 'content-type': 'application/x-www-form-urlencoded',
  516. },
  517. body: unsafeWindow.fetchTmpBody,
  518. method: 'POST',
  519. mode: 'cors',
  520. credentials: 'include',
  521. }
  522. );
  523. const responseJson = await response.json();
  524. const excelData = responseJson.data.operloglist.reduce(
  525. (result, current) => {
  526. const typeMapping = {
  527. 9: '新增部门',
  528. 10: '删除部门',
  529. 11: '移动部门',
  530. 13: '删除成员',
  531. 14: '新增成员',
  532. 15: '更改成员信息',
  533. 21: '更改部门信息',
  534. 23: '登录后台',
  535. 25: '发送邀请',
  536. 36: '修改管理组管理员列表',
  537. 35: '修改管理组应用权限',
  538. 34: '修改管理组通讯录权限',
  539. 88: '修改汇报规则',
  540. 120: '导出相关操作记录',
  541. 162: '批量设置成员信息',
  542. };
  543. const optTypeArray = {
  544. 0: '全部',
  545. 3: '成员与部门变更',
  546. 2: '权限管理变更',
  547. 12: '企业信息管理',
  548. 11: '通讯录与聊天管理',
  549. 13: '外部联系人管理',
  550. 8: '应用变更',
  551. 7: '其他',
  552. };
  553. return [
  554. ...result,
  555. [
  556. convertTimestampToTime(current.operatetime),
  557. current.op_name,
  558. optTypeArray[current.type_oper_1],
  559. typeMapping[current.type] || '其他',
  560. current.data,
  561. current.ip,
  562. ],
  563. ];
  564. },
  565. [
  566. [
  567. '时间',
  568. '操作者',
  569. '操作类型',
  570. '操作行为',
  571. '相关数据',
  572. '操作者IP',
  573. ],
  574. ]
  575. );
  576. downloadCSV(excelData, '管理端操作记录');
  577. });
  578. });
  579. },
  580. },
  581. // 监听钉钉审计日志
  582. {
  583. filter: (node, _mutation) => {
  584. return (
  585. node.querySelectorAll &&
  586. Array.from(
  587. node.querySelectorAll(
  588. '.audit-content tbody tr>td:nth-child(4)>div'
  589. )
  590. ).filter((e) =>
  591. ['删除部门', '添加部门'].includes(e.innerText)
  592. ).length > 0
  593. );
  594. },
  595. action: (node, _mutation) => {
  596. node
  597. .querySelectorAll(
  598. '.audit-content tbody tr>td:nth-child(4)>div'
  599. )
  600. .forEach((ele) => {
  601. const parentDiv = ele.closest('tr.dtd-table-row');
  602. if (parentDiv) {
  603. parentDiv.style.display = 'none';
  604. }
  605. });
  606. },
  607. },
  608. ],
  609. },
  610. }
  611. );
  612. });
  613.  
  614. // ################### 替换请求
  615. if (
  616. unsafeWindow.location.pathname.startsWith('/wework_admin') &&
  617. !unsafeWindow.location.href.includes('loginpage_wx')
  618. ) {
  619. registerFetchModifier([
  620. {
  621. test: (url, options) => {
  622. return url.includes('/wework_admin/getAdminOperationRecord');
  623. },
  624. prerequest: (url, options) => {
  625. options.body = options.body
  626. .split('&')
  627. .reduce((result, current) => {
  628. let [key, value] = current.split('=');
  629. if (key === 'limit') {
  630. value = 500;
  631. }
  632. return [...result, `${key}=${value}`];
  633. }, [])
  634. .join('&');
  635. unsafeWindow.fetchTmpBody = options.body;
  636. return [url, options];
  637. },
  638. preresponse: async (responsePromise) => {
  639. const response = await responsePromise;
  640. let responseJson = await response.json();
  641. responseJson.data.operloglist = responseJson.data.operloglist.filter(
  642. (e) => e.type_oper_1 !== 3
  643. );
  644. responseJson.data.total = responseJson.data.operloglist.length;
  645. return new Response(JSON.stringify(responseJson), {
  646. headers: response.headers,
  647. ok: response.ok,
  648. redirected: response.redirected,
  649. status: response.status,
  650. statusText: response.statusText,
  651. type: response.type,
  652. url: response.url,
  653. });
  654. },
  655. },
  656. ]);
  657. registerXMLHttpRequestPolyfill();
  658. }
  659. })();