mpMath: 与微信公众号的斗智之旅(1)

Posted by w@hidva.com on March 1, 2025

这是一篇水文, 可是为了解决 mpMath 与微信公众号新版编辑器不兼容的原因, 我浪费了我美丽的周日下午, 不把这个经历水出来我实在意难平啊!意难平!!

mpMath 的一键公式转换一直是我做出来的东西中我第二引以为豪的! 每次点击 “公式转换” 之后 mpMath 将文档中一大坨公式一一绘制成 svg 我都感到了多巴胺爆棚. 而且 mpMath 也帮我认识好多数学科普 up 主. 可是! 最近! 她不能工作了!

P.S. 第一自豪的当时是我的 hidva/as2cfg 啦, 将一大坨汇编代码绘制成一个井然有序的 Control Flow Graph 啧啧那感觉爽爆了!

现场很奇怪, 点了 “公式转换” 之后页面便被清空了. 一脸懵逼, 我对前端可是一窍不通啊, 当初一键公式转换就是在 Qwen, ChatGPT 一大帮子 AI 帮助下才整出来的, 而且我真不想花时间在这上面. 可是没有办法, 硬着头皮给编辑器主页面 div 元素下了断点, 准备看看是谁修改了这个 div.

// https://res.wx.qq.com/mpres/zh_CN/htmledition/js/default~media/appmsg_edit_v2_gray~media/msg_modify_fe.9ffea297.js
this.observer = window.MutationObserver && new window.MutationObserver(u => {
  for (let y = 0; y < u.length; y++)
    this.queue.push(u[y]);
  if (U && Z <= 11 && u.some(y => y.type == "childList" && y.removedNodes.length || y.type == "characterData" && y.oldValue.length > y.target.nodeValue.length))
    this.flushSoon();
  else
    this.flush()
});

如上所示, 微信通过 MutationObserver 监听到变更页面具有 mjx-container, mathml 等 tag 之后会自动移除这些 node, 我实在不理解为啥要移除掉这些人畜无害的 node. 所以解决方法也很简单, hook window.MutationObserver! 一开始是使用 Tampermonkey 直接让 window.MutationObserver 为 None:

// ==UserScript==
// @name         Disable MutationObserver
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  禁用 MutationObserver
// @match        *://*/*  // 用于定义在哪些页面上应用此脚本,你可以更改为特定网页
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    window.MutationObserver = undefined;
    window.WebKitMutationObserver = undefined; // 兼容性
  })();

P.S. 注意这个 run-at document-start 不可少, 不然 msg_modify_fe 先加载之行的话 hook 就没有效果了. 但这会影响到 appmsg_edit_v2_gray_fe.50b9e746.js 一些功能, 这个文件会直接构造 MutationObserver 对象:

MutationObserver(function() {
  Z.disconnect();
  if (V.textContent && !V.hasAttribute("data-styled")) {
      Ut(V, M.name, K, M.url, V.__MICRO_APP_LINK_PATH__)
  }
});

所以换个思路, 仅当 msg_modify_fe 文件构造 MutationObserver 时才使用 hook 后的 MutationObserver.

// ==UserScript==
// @name         wechat-hooked-MutationObserver
// @namespace    http://tampermonkey.net/
// @description  hook MutationObserver 以便让微信公众号允许 mjx-container 等 Tag.
// @author       hidva.com
// @match        *://*/*
// @match        https://mp.weixin.qq.com/cgi-bin/appmsg*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 保存原始 MutationObserver
    const OriginalMutationObserver = window.MutationObserver;

    // 创建一个包装的 MutationObserver
    window.MutationObserver = function(callback) {
        // 根据调用栈判断如果是 a.js 调用,就拒绝创建观察者
        const error = new Error();
        if (error.stack && error.stack.includes('msg_modify_fe')) {
            console.warn('MutationObserver blocked in msg_modify_fe');
            // 返回一个伪造的对象以避免错误
            return {
                observe: function(a,b) {},
                disconnect: function() {},
                takeRecords: function() { return []; }
            };
        } else {
            // 否则继续使用原始 MutationObserver
            return new OriginalMutationObserver(callback);
        }
    };

    // 保留原型链,以兼容 instanceof 操作
    window.MutationObserver.prototype = OriginalMutationObserver.prototype;
})();

还是不行, 保存时还是会被去掉所有 mjx-container. P.S. 如上这些代码全是 Qwen 生成的.

唉! 静了下心, 看来速战速决的法子行不通, 需要踏实踏实分析一下根因了. 看上去根本原因应该还是微信公众号编辑器换到 ProseMirror 了, 原有基于 ueditor 的方式便不再兼容了.

等等我注意到文件名中的 “gray” 这意味着 ProseMirror 新编辑器还在灰度中, 而我不幸地被选中灰度了. 那么意味着一定有个地方可以控制我是否参与灰度, 我希望这些东西不要是放在服务端决策的啊. 简单捋了下被混淆后的代码, __createEditor() 看起来像是 create editor 的入口, window.__MpEditor, useEditorV 若为 true, 则使用新版编辑器. 然后幸运地找到了:

var Ut = (Dt || location.href.indexOf("mpeditor=1") !== -1) && location.href.indexOf("mpeditor=0") === -1

这里 Ut 也是用于控制是否启用 new editor. 而且确实在链接中追加 mpeditor=0 便不会使用新编辑器了!