└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # LiteLoaderQQNT-BeginnerTutorial 2 | 3 | ~~随手写的还有人看,勉为其难的更新最后一次吧~~ 4 | 本文仅仅是把自己折腾的一些小东西整理了一下,倒也没有多少东西 5 | 不出意外的话是不会再写更多内容了,因为我已经对插件开发失去了兴趣 6 | [自用插件开发模板](https://github.com/nyaruhodoo/LiteLoader-NapCatCore-Template),如果我还有精力的话或许会把模板里挖的坑给填上 7 | 8 | 另外是本文不会给出太多代码,你可以结合上边的模板慢慢研究 9 | 10 | # 从 LiteLoaderQQNT 的原理说起 11 | 12 | 这里只针对一些关键部分进行说明,你可以去[阅读源码](https://github.com/LiteLoaderQQNT/LiteLoaderQQNT/tree/main/src)了解更多细节(难度很低) 13 | 14 | ## 万恶之源 15 | 16 | ```ts 17 | require('QQ/resources/app/LiteLoaderQQNT-main') 18 | require('./launcher.node').load('external_index', module) 19 | ``` 20 | 21 | 我想大家都知道 LiteLoader 如何安装,这里先无视掉完整性检查之类的东东 22 | 显然易见的能看出来在 QQ 启动之前就执行了"我们"的代码,在 HOOK 这一领域,谁先执行谁有理 23 | 在 JS 中我们不仅可以对全局 API 进行覆盖,import 的模块也可以通过 cache 进行覆盖 24 | 通过这一操作就可以绝对掌握 QQ 的运行环境 25 | 26 | 讲一个小趣事,之前某个屏蔽百度的广告的JS插件用到了 `IntersectionObserver` 百度发现后直接把这个 API 赋值为 `null` 27 | 因为油猴脚本的插入时机做不到比原逻辑更快 28 | 29 | ## 拦截 main 30 | 31 | 这里我们忽视掉所有细节直接看核心部分 32 | 33 | ```ts 34 | require.cache['electron'] = new Proxy(require.cache['electron'], { 35 | get(target, property, receiver) { 36 | const electron = Reflect.get(target, property, receiver) 37 | return property != 'exports' 38 | ? electron 39 | : new Proxy(electron, { 40 | get(target, property, receiver) { 41 | const BrowserWindow = Reflect.get(target, property, receiver) 42 | return property != 'BrowserWindow' 43 | ? BrowserWindow 44 | : new Proxy(BrowserWindow, { 45 | construct: proxyBrowserWindowConstruct, 46 | }) 47 | }, 48 | }) 49 | }, 50 | }) 51 | 52 | function proxyBrowserWindowConstruct(target, [config], newTarget) { 53 | const window = Reflect.construct( 54 | target, 55 | [ 56 | { 57 | ...config, 58 | webPreferences: { 59 | ...config.webPreferences, 60 | webSecurity: false, 61 | preload: processPreloadPath(config.webPreferences.preload), 62 | }, 63 | }, 64 | ], 65 | newTarget, 66 | ) 67 | 68 | // 加载自定义协议 69 | protocolRegister(window.webContents.session.protocol) 70 | 71 | // 加载插件 72 | loader.onBrowserWindowCreated(window) 73 | 74 | return window 75 | } 76 | ``` 77 | 78 | 用了一种很奇妙的方式拦截了 qq 对 `electron` 依赖的访问,主要是替换了 `BrowserWindow` 函数,注入自己的 `preload` 文件,并将 `window` 传递给插件的 `onBrowserWindowCreated` 79 | 80 | ## 拦截 preload & renderer 81 | 82 | 实际上这里的只是拦截 main 的后续操作,毕竟加载的文件已经完全被替换 83 | 唯一需要注意的是,preload 文件中并不支持原生的 require (polyfilled 实现),这里采用了特殊的方式注入代码 84 | 85 | ```ts 86 | // 通过自定义协议加载自己的 renderer,其中又加载了插件的 renderer 87 | document.addEventListener('readystatechange', () => { 88 | if (document.readyState == 'interactive') { 89 | const script = document.createElement('script') 90 | script.type = 'module' 91 | script.src = `local://root/src/renderer.js` 92 | document.head.prepend(script) 93 | } 94 | }) 95 | 96 | // 通过读取文件的方式加载其他 preload 97 | const runPreloadScript = (code) => 98 | binding.createPreloadScript(` 99 | (async function(require, process, Buffer, global, setImmediate, clearImmediate, exports, module) { 100 | ${code} 101 | }); 102 | `)(...arguments) 103 | ``` 104 | 105 | 看到这里我想你已经对 LiteLoaderQQNT 有一个初步了解了,~~又或者对 JS 的 HOOK 有了个初步的了解~~ 106 | 它的代码十分精简,目的就是将插件的代码注入到 QQ 中 107 | 108 | # 调试 109 | 110 | ~~[官方文档](https://liteloaderqqnt.github.io/docs/introduction.html#%E8%B0%83%E8%AF%95%E4%BB%A3%E7%A0%81) 我知道你们也懒得看~~ 111 | 112 | 在 qq 安装文件夹下执行 `./qq.exe --enable-logging` 即可打开调试终端 113 | 114 | ![](https://files.catbox.moe/qygfyd.png) 115 | 116 | 调试渲染层可以安装这个插件[chii-devtools](https://github.com/mo-jinran/chii-devtools/tree/v4),可以在 QQ 中使用 F12 打开开发者工具 117 | 需要注意的是这是一个远程调试手段,他并不是真的 devTools,对于某些数据结构它无法很好的渲染 118 | 而且它无法对预渲染层进行调试,`preload.js` 和 chrome 扩展一样是在 `Isolated World` 里跑的,但 chii 没 hook `Isolated World` 里的 `console.log` 119 | ~~也有其他魔法可以进行调试,但我觉得终端已经足够了~~ 120 | 121 | # 插件到底能做什么? 122 | 123 | 你已经获得了一个本地Node运行环境,并且可以去触碰QQ的部分主线程逻辑以及渲染层逻辑 124 | 简单来说,唯一限制你的是你的想象力 125 | 126 | # 添加设置界面 127 | 128 | [相关代码已进行迁移](https://github.com/nyaruhodoo/LiteLoader-Wrapper-Template/blob/master/src/renderer/configView/App.vue) 129 | 因为集成了 Vue 现在添加设计界面已经是很随意的事情了,完全看你想怎么做,哪怕直接安装一个[element](https://element-plus.org/zh-CN/)都没什么问题 130 | 模板中只针对LL提供的部分组件额外封装了一层便于绑定 `v-model` 131 | 132 | ## 初始化与卸载逻辑 133 | 134 | 虽然我帮你解决了配置文件的更新问题,但你的插件如何去同步还是需要自己去做 135 | 一个比较简单的办法是,你在代码中直接通过 `Utils.getConfig` 去访问最新的配置文件 136 | 但如果你的逻辑涉及到 `CSS` 之类的可能就要面临先卸载再初始化一次了 137 | 比如我自己的插件则是把所有事件 remove 后重新绑定 138 | 139 | ```ts 140 | export const initGrabRedBag = async (config?: ConfigType) => { 141 | Utils.log('初始化成功', globalThis.authData) 142 | authData = globalThis.authData 143 | wrapperEmitter.removeAllListeners(EventEnum.onRecvMsg) 144 | ipcMain.removeAllListeners(`${slug}:update`) 145 | const newConfig = config ?? (await Utils.getConfig()) 146 | 147 | wrapperEmitter.addListener(EventEnum.onRecvMsg, async ({ args }) => {}) 148 | 149 | ipcMain.on(`${slug}:update`, (_, updateConfig: ConfigType) => { 150 | initGrabRedBag(updateConfig) 151 | }) 152 | } 153 | ``` 154 | 155 | # 在 IPC 做点什么 156 | 157 | 如果你对于 IPC 不太了解,可以先阅读 [electron 官方文档](https://www.electronjs.org/zh/docs/latest/tutorial/ipc) 158 | 在这里我们简单的把 NTQQ 的 Vue 部分理解为前端,IPC 部分理解为后端 159 | 只要 hook 了 IPC 部分,那么大部分功能其实也都可以实现了 160 | [相关代码已进行迁移](https://github.com/nyaruhodoo/LiteLoader-NapCatCore-Template/blob/master/src/main/hook/hookIPC.ts),这里仅仅对 IPC 做一个比较简单的介绍 161 | 162 | 众所周知 IPC 是可以双向通信的,也就是主线程和渲染线程的通信,举个栗子 163 | 164 | ```ts 165 | // 此处为渲染线程向主线程 emit 的消息 166 | ;[ 167 | { frameId: 1, processId: 5 }, 168 | false, 169 | // 这里的2,代表的是qq主窗口,每个窗口都具备自己的标识ID 170 | 'IPC_UP_2', 171 | [ 172 | { 173 | // request 代表请求主线程去做某件事 174 | type: 'request', 175 | // 该id用于主线程向渲染线程发送响应 176 | callbackId: '57ee753d-e390-46d0-b785-abff293786d4', 177 | // 该参数搭配下面的 checkHasMultipleQQ 会形成一个函数的调用 178 | eventName: 'ns-BusinessApi-2', 179 | }, 180 | ['checkHasMultipleQQ'], 181 | ], 182 | ] 183 | ``` 184 | 185 | ```ts 186 | // 此处为主线程向渲染线程 send 的消息 187 | ;[ 188 | 'IPC_DOWN_2', 189 | { 190 | callbackId: '57ee753d-e390-46d0-b785-abff293786d4', 191 | promiseStatue: 'full', 192 | type: 'response', 193 | eventName: 'ns-BusinessApi-2', 194 | }, 195 | // 只需关注这里即可,代表的是返回值 196 | true, 197 | ] 198 | // 主线程除了会发送 response 也会发送 request 类型事件 199 | ;[ 200 | ('IPC_DOWN_2', 201 | { type: 'request', eventName: 'ns-ntApi-2' }, 202 | [ 203 | { 204 | // 比如收到新消息时情况则会反过来,是主线程请求渲染线程去做某些事,会派发一个 cmd 事件 205 | cmdName: 'nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated', 206 | cmdType: 'event', 207 | // 携带参数 208 | payload: [], 209 | }, 210 | ]), 211 | ] 212 | ``` 213 | 214 | # 在 Wrapper 做点什么 215 | 216 | 这里的 Wrapper 指的是 QQ 中的 `wrapper.node`,你可以将它理解为 QQ 的底层依赖,其中提供了一套 API,当主线程收到 IPC 消息时会去调用 wrapper 中对应的函数 217 | 你可能会好奇 IPC 与 Wrapper 有什么区别,让我举个小栗子你就知道了 218 | 在 IPC 层监听新消息时需要去监听 `nodeIKernelMsgListener/onRecvActiveMsg` ,如果你已经这样做了或许会发现收到的消息并不完整时不时会丢掉很多消息,这是因为客户端做了一些限制只会获取已激活窗口下消息事件 219 | 那么我们如果在 wrapper 中监听 `NodeIQQNTWrapperSession/create/getMsgService/addKernelMsgListener/onRecvMsg` 你就会发现一个消息都不会丢失 220 | 简单来说 wrapper 中的 API 更加纯净,并且脱离客户端限制 221 | 222 | [相关代码已进行迁移](https://github.com/nyaruhodoo/LiteLoader-NapCatCore-Template/blob/master/src/main/hook/hookWrapper.ts) 223 | 224 | # 在渲染层做点什么 225 | 226 | 渲染层实际上并没有太多需要注意的事情,硬要说的话倒是有几个点 227 | - LL会对所有窗口注入渲染层代码,需要你自行判断url决定代码是否执行 228 | - 可以通过`window.app`拿到一些提前注入好的数据(几个vue相关的属性,自己翻吧) 229 | - 可以通过`window.ipcRenderer`直接监听ipc相关事件 230 | 231 | ## Hook Vue 232 | 233 | 未完待续,鸽一会 234 | --------------------------------------------------------------------------------