mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
feat: mac发送能力重构为libtmwechat
- 发送改走本地host, 在微信登录时触发 - 删除旧OneBot及对应UI控制面
This commit is contained in:
@@ -1,436 +0,0 @@
|
||||
/*
|
||||
* wechat_chatter runtime integration
|
||||
*
|
||||
* Upstream: https://github.com/yincongcyincong/wechat_chatter
|
||||
* Runtime version: v0.0.18
|
||||
* Upstream license: GNU General Public License version 3 (GPL-3.0)
|
||||
*
|
||||
* This file applies local compatibility patches to the upstream
|
||||
* onebot/script.js. See docs/third-party/wechat-chatter/NOTICE.md.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-require-imports */
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const fs = require('node:fs')
|
||||
const os = require('node:os')
|
||||
const path = require('node:path')
|
||||
|
||||
const release = 'v0.0.18'
|
||||
const asset = 'onebot_mac_arm64.tar.gz'
|
||||
const url = `https://github.com/yincongcyincong/wechat_chatter/releases/download/${release}/${asset}`
|
||||
const projectRoot = path.resolve(__dirname, '..')
|
||||
const outputDir = path.join(
|
||||
projectRoot,
|
||||
'resources',
|
||||
'connectors',
|
||||
'wechat-personal',
|
||||
'darwin-arm64'
|
||||
)
|
||||
const archive = path.join(os.tmpdir(), `wechat-chatter-${release}-${asset}`)
|
||||
const appleSilicon =
|
||||
process.platform === 'darwin' &&
|
||||
execFileSync('/usr/sbin/sysctl', ['-n', 'hw.optional.arm64'], { encoding: 'utf8' }).trim() === '1'
|
||||
|
||||
if (!appleSilicon) {
|
||||
throw new Error('个人微信发送运行时当前仅支持 macOS arm64')
|
||||
}
|
||||
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
let archiveReady = false
|
||||
if (fs.existsSync(archive)) {
|
||||
try {
|
||||
execFileSync('/usr/bin/tar', ['-tzf', archive], { stdio: 'ignore' })
|
||||
archiveReady = true
|
||||
console.log(`[wechat-personal] 复用已下载归档:${archive}`)
|
||||
} catch {
|
||||
// The archive is partial or invalid; curl will resume it below.
|
||||
}
|
||||
}
|
||||
if (!archiveReady) {
|
||||
console.log(`[wechat-personal] 下载 ${release},支持断点续传:${archive}`)
|
||||
execFileSync(
|
||||
'/usr/bin/curl',
|
||||
['--http1.1', '-L', '--fail', '--retry', '3', '--continue-at', '-', '--output', archive, url],
|
||||
{ stdio: 'inherit' }
|
||||
)
|
||||
}
|
||||
|
||||
const extractionDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wechat-chatter-extract-'))
|
||||
console.log(`[wechat-personal] 解压并原子安装到 ${outputDir}`)
|
||||
try {
|
||||
execFileSync('/usr/bin/tar', ['-xzf', archive, '-C', extractionDir], { stdio: 'inherit' })
|
||||
fs.mkdirSync(path.join(outputDir, 'onebot'), { recursive: true })
|
||||
fs.mkdirSync(path.join(outputDir, 'wechat_version'), { recursive: true })
|
||||
fs.copyFileSync(
|
||||
path.join(extractionDir, 'onebot', 'script.js'),
|
||||
path.join(outputDir, 'onebot', 'script.js')
|
||||
)
|
||||
fs.cpSync(path.join(extractionDir, 'wechat_version'), path.join(outputDir, 'wechat_version'), {
|
||||
recursive: true,
|
||||
force: true
|
||||
})
|
||||
const stagedExecutable = path.join(outputDir, 'onebot', `.onebot-${process.pid}.tmp`)
|
||||
fs.copyFileSync(path.join(extractionDir, 'onebot', 'onebot'), stagedExecutable)
|
||||
fs.chmodSync(stagedExecutable, 0o755)
|
||||
fs.renameSync(stagedExecutable, path.join(outputDir, 'onebot', 'onebot'))
|
||||
} finally {
|
||||
fs.rmSync(extractionDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
const executable = path.join(outputDir, 'onebot', 'onebot')
|
||||
const script = path.join(outputDir, 'onebot', 'script.js')
|
||||
const config = path.join(outputDir, 'wechat_version', '4_1_11_53_mac.json')
|
||||
for (const required of [executable, script, config]) {
|
||||
if (!fs.existsSync(required)) throw new Error(`运行时文件缺失:${required}`)
|
||||
}
|
||||
|
||||
function patchPerSendPayload(scriptPath) {
|
||||
let source = fs.readFileSync(scriptPath, 'utf8')
|
||||
if (!source.includes('var activeTriggerX1Payload = ptr(0);')) return
|
||||
|
||||
const activeSend = ` const payloadData = hexToByteArray(payloadHex);
|
||||
activeTriggerX1Payload = Memory.alloc(payloadData.length);
|
||||
activeTriggerX1Payload.writeByteArray(payloadData);
|
||||
activeTriggerX1Payload.add(0x18).writePointer(info.cgiAddr);
|
||||
activeTriggerX1Payload.add(0xb8).writePointer(activeTriggerX1Payload.add(0xc0));
|
||||
activeTriggerX1Payload.add(0x190).writePointer(activeTriggerX1Payload.add(0x198));`
|
||||
const upstreamSend = ` const payloadData = hexToByteArray(payloadHex);
|
||||
triggerX1Payload.writeByteArray(payloadData);
|
||||
triggerX1Payload.add(0x18).writePointer(info.cgiAddr);
|
||||
triggerX1Payload.add(0xb8).writePointer(triggerX1Payload.add(0xc0));
|
||||
triggerX1Payload.add(0x190).writePointer(triggerX1Payload.add(0x198));`
|
||||
if (!source.includes(activeSend)) throw new Error('无法定位 wechat_chatter 连续发送补丁位置')
|
||||
source = source
|
||||
.replace(
|
||||
'var triggerX1Payload;\nvar activeTriggerX1Payload = ptr(0);\nvar triggerX0;',
|
||||
'var triggerX1Payload;\nvar triggerX0;'
|
||||
)
|
||||
.replace(activeSend, upstreamSend)
|
||||
.replace(
|
||||
' MMStartTask(triggerX0, activeTriggerX1Payload);',
|
||||
' MMStartTask(triggerX0, triggerX1Payload);'
|
||||
)
|
||||
.replace(
|
||||
' activeTriggerX1Payload = ptr(0);\n console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);',
|
||||
' console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);'
|
||||
)
|
||||
.replace(
|
||||
'\t\t\t\tpendingSendMsgType = "";\n\t\t\t\tactiveTriggerX1Payload = ptr(0);\n\t\t\t\treturn',
|
||||
'\t\t\t\tpendingSendMsgType = "";\n\t\t\t\treturn'
|
||||
)
|
||||
fs.writeFileSync(scriptPath, source)
|
||||
console.log('[wechat-personal] 已恢复原生发送 payload 布局')
|
||||
}
|
||||
|
||||
function patchSendContextCapture(scriptPath, strict = true) {
|
||||
let source = fs.readFileSync(scriptPath, 'utf8')
|
||||
if (source.includes('function isLikelySendContext(')) return
|
||||
const original = `function AttachSendFunc() {
|
||||
Interceptor.attach(sendFuncAddr.add(0x10), {
|
||||
onEnter: function (args) {
|
||||
|
||||
if (triggerX1Payload) {
|
||||
return
|
||||
}
|
||||
|
||||
triggerX0 = this.context.x0;
|
||||
triggerX1Payload = this.context.x1;
|
||||
console.log(\`[+] 捕获到 StartTask 调用,X0:\${triggerX0}, Payload: \${triggerX1Payload}\`);
|
||||
}
|
||||
})
|
||||
}`
|
||||
const patched = `function isLikelySendContext(candidateX0, candidateX1) {
|
||||
try {
|
||||
if (!isReadablePointer(candidateX0) || !isReadablePointer(candidateX1)) return false;
|
||||
var manager = readPointerIfReadable(candidateX0.add(0x18));
|
||||
var cgi = readUtf8StringIfReadable(readPointerIfReadable(candidateX1.add(0x18)));
|
||||
console.log("[debug] StartTask candidate x0=" + candidateX0 + " x1=" + candidateX1 + " x0+0x18=" + manager + " cgi=" + cgi);
|
||||
return !manager.equals(ptr(0));
|
||||
} catch (e) {
|
||||
console.error("[debug] StartTask candidate inspect failed: " + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function AttachSendFunc() {
|
||||
Interceptor.attach(sendFuncAddr.add(0x10), {
|
||||
onEnter: function (args) {
|
||||
if (triggerX1Payload) return;
|
||||
var candidateX0 = this.context.x0;
|
||||
var candidateX1 = this.context.x1;
|
||||
if (!isLikelySendContext(candidateX0, candidateX1)) return;
|
||||
triggerX0 = candidateX0;
|
||||
triggerX1Payload = candidateX1;
|
||||
console.log(\`[+] 捕获到有效 StartTask 上下文,X0:\${triggerX0}, Payload: \${triggerX1Payload}\`);
|
||||
}
|
||||
})
|
||||
}`
|
||||
if (!source.includes(original)) {
|
||||
if (strict) throw new Error('无法定位 StartTask 上下文 Hook')
|
||||
return
|
||||
}
|
||||
source = source.replace(original, patched)
|
||||
fs.writeFileSync(scriptPath, source)
|
||||
}
|
||||
|
||||
function patchVoiceAudioBuffer(scriptPath) {
|
||||
let source = fs.readFileSync(scriptPath, 'utf8')
|
||||
if (source.includes('voiceAudioDataAddr = Memory.alloc(audioLen + 1);')) return
|
||||
|
||||
const staticAllocation = 'voiceAudioDataAddr = Memory.alloc(5 * 1024 * 1024); // 预分配5MB'
|
||||
if (!source.includes(staticAllocation)) {
|
||||
throw new Error('无法定位 wechat_chatter 语音缓冲区')
|
||||
}
|
||||
source = source.replace(
|
||||
staticAllocation,
|
||||
'voiceAudioDataAddr = Memory.alloc(1); // 上传前按语音长度重新分配'
|
||||
)
|
||||
const audioLengthMarker = ' const audioLen = audioBytes.length;\n'
|
||||
if (!source.includes(audioLengthMarker)) {
|
||||
throw new Error('无法定位 wechat_chatter 语音上传逻辑')
|
||||
}
|
||||
source = source.replace(
|
||||
audioLengthMarker,
|
||||
`${audioLengthMarker} voiceAudioDataAddr = Memory.alloc(audioLen + 1);\n`
|
||||
)
|
||||
fs.writeFileSync(scriptPath, source)
|
||||
console.log('[wechat-personal] 已应用按语音长度分配上传缓冲区补丁')
|
||||
}
|
||||
|
||||
function patchImageHookReadiness(scriptPath) {
|
||||
let source = fs.readFileSync(scriptPath, 'utf8')
|
||||
if (
|
||||
source.includes('捕获到图片上传上下文,uploadGlobalX0') &&
|
||||
source.includes('图片上传 Hook Setup Complete')
|
||||
)
|
||||
return
|
||||
const original = `\t\t\tuploadGlobalX0 = this.context.x0;`
|
||||
const patched = `\t\t\tconst capturedUploadX0 = this.context.x0;
|
||||
\t\t\tif (uploadGlobalX0.equals(ptr(0)) && !capturedUploadX0.equals(ptr(0))) {
|
||||
\t\t\t\tconsole.log("[+] 捕获到图片上传上下文,uploadGlobalX0:" + capturedUploadX0);
|
||||
\t\t\t}
|
||||
\t\t\tuploadGlobalX0 = capturedUploadX0;`
|
||||
if (!source.includes(original)) throw new Error('无法定位 wechat_chatter 图片 Hook 状态补丁位置')
|
||||
source = source.replace(original, patched)
|
||||
source = source.replace(
|
||||
' })\n}\n\n\n\nfunction patchCdnOnComplete()',
|
||||
' })\n console.log("[+] 图片上传 Hook Setup Complete.");\n}\n\n\n\nfunction patchCdnOnComplete()'
|
||||
)
|
||||
fs.writeFileSync(scriptPath, source)
|
||||
console.log('[wechat-personal] 已应用图片 Hook 状态补丁')
|
||||
}
|
||||
|
||||
// Backport of wechat_chatter PR #36 by @Leslielu:
|
||||
// https://github.com/yincongcyincong/wechat_chatter/pull/36
|
||||
// TraceMemo adds the verified macOS WeChat 4.1.11.53 addresses.
|
||||
// macOS WeChat 4.1.11.53, located and verified by TraceMemo
|
||||
function patchCdnColdStart(scriptPath) {
|
||||
let source = fs.readFileSync(scriptPath, 'utf8')
|
||||
if (source.includes('function resolveCdnManager()')) return
|
||||
|
||||
const initAddresses = ` uploadImageAddr = baseAddr.add({{.uploadImageAddr}});
|
||||
cndOnCompleteAddr = baseAddr.add({{.cndOnCompleteAddr}});`
|
||||
const patchedInitAddresses = ` uploadImageAddr = baseAddr.add({{.uploadImageAddr}});
|
||||
cndOnCompleteAddr = baseAddr.add({{.cndOnCompleteAddr}});
|
||||
// 冷启动 CdnManager 解析(旧版本缺少可选键时保持 hook 捕获行为)
|
||||
{{if .cdnGetServiceAddr}}cdnGetServiceAddr = baseAddr.add({{.cdnGetServiceAddr}});{{end}}
|
||||
{{if .cdnManagerGetterAddr}}cdnManagerGetterAddr = baseAddr.add({{.cdnManagerGetterAddr}});{{end}}`
|
||||
if (!source.includes(initAddresses)) throw new Error('下载的微信版本配置与当前应用不兼容')
|
||||
source = source.replace(initAddresses, patchedInitAddresses)
|
||||
|
||||
const downloadChunkEnd = `}
|
||||
|
||||
function fillUploadX1AndStart`
|
||||
const resolver = `}
|
||||
|
||||
// 上传和下载共用同一个 mars::cdn::CdnManager。冷启动时通过服务定位器
|
||||
// 取得 [ctx + 0x40],避免必须先手动发送图片才能让 Hook 捕获上下文。
|
||||
function resolveCdnManager() {
|
||||
if (cdnGetServiceAddr.equals(ptr(0)) || cdnManagerGetterAddr.equals(ptr(0))) {
|
||||
return ptr(0);
|
||||
}
|
||||
try {
|
||||
// libc++ SSO 短字符串:数据在 +0,长度写在 +0x17。
|
||||
var strDefault = Memory.alloc(24);
|
||||
strDefault.writeUtf8String("default");
|
||||
strDefault.add(0x17).writeU8(7);
|
||||
|
||||
var getService = new NativeFunction(cdnGetServiceAddr, 'pointer', ['pointer']);
|
||||
var svc = getService(strDefault);
|
||||
if (!isReadablePointer(svc)) {
|
||||
console.error("[!] GetService(\\"default\\") 返回不可读: " + svc);
|
||||
return ptr(0);
|
||||
}
|
||||
var getCtx = new NativeFunction(cdnManagerGetterAddr, 'pointer', ['pointer']);
|
||||
var ctx = getCtx(svc);
|
||||
if (!isReadablePointer(ctx)) {
|
||||
console.error("[!] CdnManager getter 返回不可读: " + ctx);
|
||||
return ptr(0);
|
||||
}
|
||||
var mgr = readPointerIfReadable(ctx.add(0x40));
|
||||
if (!isReadablePointer(mgr)) {
|
||||
console.error("[!] ctx+0x40 管理器指针不可读: ctx=" + ctx);
|
||||
return ptr(0);
|
||||
}
|
||||
return mgr;
|
||||
} catch (e) {
|
||||
console.error("[!] resolveCdnManager 异常: " + e);
|
||||
return ptr(0);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCdnManagerX0() {
|
||||
if (uploadGlobalX0.equals(ptr(0)) && downloadGlobalX0 && !downloadGlobalX0.equals(ptr(0))) {
|
||||
uploadGlobalX0 = downloadGlobalX0;
|
||||
console.log("[+] downloadGlobalX0 回填 uploadGlobalX0: " + uploadGlobalX0);
|
||||
}
|
||||
if ((!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) && !uploadGlobalX0.equals(ptr(0))) {
|
||||
downloadGlobalX0 = uploadGlobalX0;
|
||||
console.log("[+] uploadGlobalX0 回填 downloadGlobalX0: " + downloadGlobalX0);
|
||||
}
|
||||
if (uploadGlobalX0.equals(ptr(0))) {
|
||||
var mgr = resolveCdnManager();
|
||||
if (!mgr.equals(ptr(0))) {
|
||||
uploadGlobalX0 = mgr;
|
||||
if (!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) {
|
||||
downloadGlobalX0 = mgr;
|
||||
}
|
||||
console.log("[+] 冷启动服务定位器解析 CdnManager: " + mgr);
|
||||
}
|
||||
}
|
||||
return !uploadGlobalX0.equals(ptr(0));
|
||||
}
|
||||
|
||||
function fillUploadX1AndStart`
|
||||
if (!source.includes(downloadChunkEnd)) throw new Error('无法定位 wechat_chatter 媒体上传逻辑')
|
||||
source = source.replace(downloadChunkEnd, resolver)
|
||||
|
||||
const declarations = 'var uploadImageAddr;\n'
|
||||
const patchedDeclarations =
|
||||
'var uploadImageAddr;\nvar cdnGetServiceAddr = ptr(0);\nvar cdnManagerGetterAddr = ptr(0);\n'
|
||||
if (!source.includes(declarations)) throw new Error('无法定位 wechat_chatter 媒体地址声明')
|
||||
source = source.replace(declarations, patchedDeclarations)
|
||||
|
||||
const uploadGuard = `function fillUploadX1AndStart(idAddr, pathAddr, x1Buffer, receiver, md5, filePath, payloadHex) {
|
||||
if (uploadGlobalX0.equals(ptr(0))) {`
|
||||
const patchedUploadGuard = `function fillUploadX1AndStart(idAddr, pathAddr, x1Buffer, receiver, md5, filePath, payloadHex) {
|
||||
if (uploadGlobalX0.equals(ptr(0))) {
|
||||
ensureCdnManagerX0();
|
||||
}
|
||||
if (uploadGlobalX0.equals(ptr(0))) {`
|
||||
if (!source.includes(uploadGuard)) throw new Error('无法定位 wechat_chatter 媒体上传入口')
|
||||
source = source.replace(uploadGuard, patchedUploadGuard)
|
||||
|
||||
const voiceGuard = `function triggerUploadVoice(receiver, voicePath, payloadHex, audioDataHex, durationMs) {
|
||||
if (uploadGlobalX0.equals(ptr(0))) {`
|
||||
const patchedVoiceGuard = `function triggerUploadVoice(receiver, voicePath, payloadHex, audioDataHex, durationMs) {
|
||||
if (uploadGlobalX0.equals(ptr(0))) {
|
||||
ensureCdnManagerX0();
|
||||
}
|
||||
if (uploadGlobalX0.equals(ptr(0))) {`
|
||||
if (!source.includes(voiceGuard)) throw new Error('无法定位 wechat_chatter 语音上传入口')
|
||||
source = source.replace(voiceGuard, patchedVoiceGuard)
|
||||
|
||||
const uploadHook = `\t\t\tuploadGlobalX0 = capturedUploadX0;`
|
||||
const patchedUploadHook = `\t\t\tuploadGlobalX0 = capturedUploadX0;
|
||||
if ((!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) && !capturedUploadX0.equals(ptr(0))) {
|
||||
downloadGlobalX0 = capturedUploadX0;
|
||||
console.log("[+] 上传hook回填 downloadGlobalX0: " + downloadGlobalX0);
|
||||
}`
|
||||
if (!source.includes(uploadHook)) throw new Error('无法定位 wechat_chatter 图片 Hook')
|
||||
source = source.replace(uploadHook, patchedUploadHook)
|
||||
|
||||
const downloadHook = ` downloadGlobalX0 = this.context.x0;`
|
||||
const patchedDownloadHook = ` downloadGlobalX0 = this.context.x0;
|
||||
if (uploadGlobalX0.equals(ptr(0)) && !downloadGlobalX0.equals(ptr(0))) {
|
||||
uploadGlobalX0 = downloadGlobalX0;
|
||||
console.log("[+] 下载hook回填 uploadGlobalX0: " + uploadGlobalX0);
|
||||
}`
|
||||
if (!source.includes(downloadHook)) throw new Error('无法定位 wechat_chatter 下载 Hook')
|
||||
source = source.replace(downloadHook, patchedDownloadHook)
|
||||
|
||||
const downloadGuard = `function triggerDownload(receiver, cdnUrl, aesKey, filePath, fileType) {
|
||||
if (!downloadGlobalX0) {`
|
||||
const patchedDownloadGuard = `function triggerDownload(receiver, cdnUrl, aesKey, filePath, fileType) {
|
||||
if (!downloadGlobalX0 || downloadGlobalX0.equals(ptr(0))) {
|
||||
ensureCdnManagerX0();
|
||||
}
|
||||
if (!downloadGlobalX0) {`
|
||||
if (!source.includes(downloadGuard)) throw new Error('无法定位 wechat_chatter 媒体下载入口')
|
||||
source = source.replace(downloadGuard, patchedDownloadGuard)
|
||||
|
||||
fs.writeFileSync(scriptPath, source)
|
||||
console.log('[wechat-personal] 已应用 CdnManager 冷启动解析补丁')
|
||||
}
|
||||
|
||||
function patchCdnColdStartConfig(configPath) {
|
||||
const source = fs.readFileSync(configPath, 'utf8')
|
||||
let config
|
||||
try {
|
||||
config = JSON.parse(source)
|
||||
} catch {
|
||||
throw new Error('4.1.11.53 版本配置不是有效 JSON')
|
||||
}
|
||||
config.cdnGetServiceAddr = '0x50a15d0'
|
||||
config.cdnManagerGetterAddr = '0x5259290'
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
|
||||
console.log('[wechat-personal] 已写入 4.1.11.53 CdnManager 地址')
|
||||
}
|
||||
|
||||
function patchWechatCoreModuleBase(scriptPath) {
|
||||
let source = fs.readFileSync(scriptPath, 'utf8')
|
||||
if (source.includes('WeChat core module base:')) return
|
||||
const initMarker = 'function initAddresses() {'
|
||||
const initIndex = source.indexOf(initMarker)
|
||||
if (initIndex < 0 || !source.startsWith('var targetPath = ')) {
|
||||
throw new Error('无法定位 wechat_chatter 基址扫描逻辑')
|
||||
}
|
||||
const patchedHeader = `var targetPath = "/Applications/WeChat.app/Contents/Resources/wechat.dylib";
|
||||
var module = Process.enumerateModules().find(function(m) {
|
||||
return m.path === targetPath || m.path.endsWith("/Contents/Resources/wechat.dylib");
|
||||
});
|
||||
if (!module) {
|
||||
throw new Error("[-] Cannot find WeChat core module: " + targetPath);
|
||||
}
|
||||
var moduleBase = module.base;
|
||||
var baseAddr = moduleBase;
|
||||
console.log("[+] WeChat core module base: " + baseAddr + " path=" + module.path);
|
||||
setImmediate(initAddresses);
|
||||
|
||||
`
|
||||
source = patchedHeader + source.slice(initIndex)
|
||||
fs.writeFileSync(scriptPath, source)
|
||||
console.log('[wechat-personal] 已应用微信核心模块基址补丁')
|
||||
}
|
||||
|
||||
function addModifiedWorkNotice(scriptPath) {
|
||||
let source = fs.readFileSync(scriptPath, 'utf8')
|
||||
if (source.includes('TraceMemo wechat_chatter compatibility modifications')) return
|
||||
|
||||
const notice = `/*
|
||||
* TraceMemo wechat_chatter compatibility modifications
|
||||
* Modified: 2026-08-17
|
||||
* Upstream: https://github.com/yincongcyincong/wechat_chatter
|
||||
* Runtime version: v0.0.18
|
||||
* License: GNU General Public License version 3 (GPL-3.0)
|
||||
* Changes: WeChat module discovery, per-send payload isolation, dynamic voice upload buffers,
|
||||
* CdnManager cold-start resolution, media hook backfill, and image Hook readiness logging.
|
||||
* These modifications are not provided by the upstream author.
|
||||
*/
|
||||
|
||||
`
|
||||
source = notice + source
|
||||
fs.writeFileSync(scriptPath, source)
|
||||
console.log('[wechat-personal] 已写入 GPL 修改声明')
|
||||
}
|
||||
|
||||
patchWechatCoreModuleBase(script)
|
||||
patchPerSendPayload(script)
|
||||
patchSendContextCapture(script)
|
||||
patchVoiceAudioBuffer(script)
|
||||
patchImageHookReadiness(script)
|
||||
patchCdnColdStartConfig(config)
|
||||
patchCdnColdStart(script)
|
||||
addModifiedWorkNotice(script)
|
||||
fs.chmodSync(executable, 0o755)
|
||||
console.log('[wechat-personal] 运行时准备完成')
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const crypto = require('node:crypto')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const REQUIRED_FILES = ['tm-wechat-host', 'libtmwechat.dylib', 'runtime-manifest.json']
|
||||
|
||||
function parseArgs(argv) {
|
||||
const result = {}
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const value = argv[index]
|
||||
if (value === '--source' || value === '--target') {
|
||||
if (!argv[index + 1]) throw new Error(`${value} requires a path`)
|
||||
result[value.slice(2)] = argv[index + 1]
|
||||
index += 1
|
||||
} else {
|
||||
throw new Error(`Unknown option: ${value}`)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function sha256(filePath) {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex')
|
||||
}
|
||||
|
||||
function listFiles(root) {
|
||||
const files = []
|
||||
function visit(directory) {
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const absolute = path.join(directory, entry.name)
|
||||
if (entry.isDirectory()) visit(absolute)
|
||||
else if (entry.isFile()) files.push(path.relative(root, absolute))
|
||||
}
|
||||
}
|
||||
visit(root)
|
||||
return files.sort()
|
||||
}
|
||||
|
||||
function assertMachOArm64(filePath, label) {
|
||||
const buffer = fs.readFileSync(filePath)
|
||||
if (buffer.length < 8 || buffer.readUInt32LE(0) !== 0xfeedfacf) {
|
||||
throw new Error(`${label} is not a 64-bit Mach-O binary: ${filePath}`)
|
||||
}
|
||||
if (buffer.readUInt32LE(4) !== 0x0100000c) {
|
||||
throw new Error(`${label} is not arm64: ${filePath}`)
|
||||
}
|
||||
}
|
||||
|
||||
function readAndValidateArtifact(sourceDir) {
|
||||
if (!fs.existsSync(sourceDir)) {
|
||||
throw new Error('native runtime artifact not found: build the macOS runtime artifact first')
|
||||
}
|
||||
|
||||
const actualFiles = listFiles(sourceDir)
|
||||
const expectedFiles = [...REQUIRED_FILES].sort()
|
||||
if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
|
||||
throw new Error(`native runtime artifact has an unexpected tree: ${actualFiles.join(', ')}`)
|
||||
}
|
||||
|
||||
const manifestPath = path.join(sourceDir, 'runtime-manifest.json')
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
||||
if (manifest.runtime !== 'tm-wechat-native') throw new Error('Unexpected runtime name')
|
||||
if (manifest.platform !== 'darwin-arm64') throw new Error('Runtime platform must be darwin-arm64')
|
||||
if (manifest.architecture !== 'arm64') throw new Error('Runtime architecture must be arm64')
|
||||
if (manifest.protocolVersion !== 1) throw new Error('Unsupported runtime protocolVersion')
|
||||
if (!/^[a-f0-9]{64}$/.test(manifest.tmSendSourceSha256 || '')) {
|
||||
throw new Error('Runtime manifest has no valid tmSendSourceSha256')
|
||||
}
|
||||
if (!/^[a-f0-9]{64}$/.test(manifest.addressProfileSha256 || '')) {
|
||||
throw new Error('Runtime manifest has no valid addressProfileSha256')
|
||||
}
|
||||
const capabilities = new Set(manifest.capabilities || [])
|
||||
for (const capability of ['text', 'image', 'voice']) {
|
||||
if (!capabilities.has(capability)) throw new Error(`Runtime capability missing: ${capability}`)
|
||||
}
|
||||
if (
|
||||
!Array.isArray(manifest.supportedWechatVersions) ||
|
||||
manifest.supportedWechatVersions.length === 0
|
||||
) {
|
||||
throw new Error('Runtime manifest has no supportedWechatVersions')
|
||||
}
|
||||
|
||||
for (const relative of REQUIRED_FILES) {
|
||||
const absolute = path.join(sourceDir, relative)
|
||||
if (!fs.statSync(absolute).isFile()) throw new Error(`Runtime file missing: ${relative}`)
|
||||
}
|
||||
const hostPath = path.join(sourceDir, 'tm-wechat-host')
|
||||
const dylibPath = path.join(sourceDir, 'libtmwechat.dylib')
|
||||
assertMachOArm64(hostPath, 'tm-wechat-host')
|
||||
assertMachOArm64(dylibPath, 'libtmwechat.dylib')
|
||||
if (!fs.readFileSync(dylibPath).includes(Buffer.from(manifest.tmSendSourceSha256, 'utf8'))) {
|
||||
throw new Error('Embedded agent hash is not present in libtmwechat.dylib')
|
||||
}
|
||||
if (!fs.readFileSync(dylibPath).includes(Buffer.from(manifest.addressProfileSha256, 'utf8'))) {
|
||||
throw new Error('Embedded address profile hash is not present in libtmwechat.dylib')
|
||||
}
|
||||
|
||||
return {
|
||||
manifest,
|
||||
hashes: Object.fromEntries(
|
||||
REQUIRED_FILES.map((file) => [file, sha256(path.join(sourceDir, file))])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function syncArtifact(sourceDir, targetDir) {
|
||||
const source = readAndValidateArtifact(sourceDir)
|
||||
const parent = path.dirname(targetDir)
|
||||
const name = path.basename(targetDir)
|
||||
const staging = path.join(parent, `.${name}.prepare-${process.pid}`)
|
||||
const backup = path.join(parent, `.${name}.backup-${process.pid}`)
|
||||
|
||||
fs.mkdirSync(parent, { recursive: true })
|
||||
fs.rmSync(staging, { recursive: true, force: true })
|
||||
fs.rmSync(backup, { recursive: true, force: true })
|
||||
fs.mkdirSync(staging, { recursive: true })
|
||||
|
||||
try {
|
||||
for (const relative of REQUIRED_FILES) {
|
||||
const destination = path.join(staging, relative)
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
||||
fs.copyFileSync(path.join(sourceDir, relative), destination)
|
||||
}
|
||||
fs.chmodSync(path.join(staging, 'tm-wechat-host'), 0o755)
|
||||
|
||||
const staged = readAndValidateArtifact(staging)
|
||||
for (const relative of REQUIRED_FILES) {
|
||||
if (source.hashes[relative] !== staged.hashes[relative]) {
|
||||
throw new Error(`Packaged runtime hash mismatch: ${relative}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(targetDir)) fs.renameSync(targetDir, backup)
|
||||
try {
|
||||
fs.renameSync(staging, targetDir)
|
||||
} catch (error) {
|
||||
if (fs.existsSync(backup)) fs.renameSync(backup, targetDir)
|
||||
throw error
|
||||
}
|
||||
fs.rmSync(backup, { recursive: true, force: true })
|
||||
return staged
|
||||
} finally {
|
||||
fs.rmSync(staging, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSource(explicit) {
|
||||
if (explicit) return path.resolve(explicit)
|
||||
const fromEnv = String(process.env.TM_NATIVE_RUNTIME_DIR || '').trim()
|
||||
if (fromEnv) return path.resolve(fromEnv)
|
||||
// 本机路径写在这里即可,该文件不进仓库。
|
||||
const localConfig = path.join(__dirname, '..', '.native-runtime-source')
|
||||
if (fs.existsSync(localConfig)) {
|
||||
const configured = fs.readFileSync(localConfig, 'utf8').trim()
|
||||
if (configured) return path.resolve(configured)
|
||||
}
|
||||
throw new Error(
|
||||
'runtime artifact source is required: pass --source <dir>, set TM_NATIVE_RUNTIME_DIR, ' +
|
||||
'or write the path into .native-runtime-source'
|
||||
)
|
||||
}
|
||||
|
||||
function main() {
|
||||
const projectRoot = path.resolve(__dirname, '..')
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
const sourceDir = resolveSource(args.source)
|
||||
const targetDir = path.resolve(
|
||||
args.target || path.join(projectRoot, 'resources', 'runtime', 'darwin-arm64')
|
||||
)
|
||||
const result = syncArtifact(sourceDir, targetDir)
|
||||
console.log(`[prepare-wechat-native] source: ${sourceDir}`)
|
||||
console.log(`[prepare-wechat-native] target: ${targetDir}`)
|
||||
console.log(
|
||||
`[prepare-wechat-native] runtime: ${result.manifest.runtime}/${result.manifest.version}`
|
||||
)
|
||||
console.log(`[prepare-wechat-native] agent: ${result.manifest.tmSendSourceSha256}`)
|
||||
console.log(`[prepare-wechat-native] address profile: ${result.manifest.addressProfileSha256}`)
|
||||
console.log('[prepare-wechat-native] files: 3')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
REQUIRED_FILES,
|
||||
assertMachOArm64,
|
||||
readAndValidateArtifact,
|
||||
syncArtifact
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main()
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[prepare-wechat-native] ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user