Greasy Fork 还支持 简体中文。

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