feat: 新增MAC微信发送消息 文字转语音 功能

This commit is contained in:
Wxw-Gu
2026-08-17 18:16:14 +08:00
parent a52da455c3
commit 15811c820c
48 changed files with 7240 additions and 22 deletions
+64
View File
@@ -0,0 +1,64 @@
#!/bin/sh
set -eu
ANCHOR='com.tencent.wechat.update'
ANCHOR_FILE='/etc/pf.anchors/wechat-update'
PF_CONF='/etc/pf.conf'
BACKUP_DIR='/etc/pf.conf.wechat-backups'
CACHE_DIR="${HOME}/Library/Caches/com.tencent.xinWeChat"
domains='dldir1.qq.com dldir2.qq.com dldir3.qq.com dldir1v6.qq.com'
die() { printf '%s\n' "error: $*" >&2; exit 1; }
need_root() { [ "$(id -u)" -eq 0 ] || die '请使用 sudo 执行此脚本'; }
resolve_ips() {
command -v dig >/dev/null 2>&1 || die '需要 digmacOS 通常已内置)'
for domain in $domains; do
dig +short A "$domain"
dig +short AAAA "$domain"
done | awk '/^[0-9]+(\.[0-9]+){3}$/ || /^[0-9A-Fa-f:]+:[0-9A-Fa-f:]+/' | sort -u
}
enable() {
need_root
ips="$(resolve_ips)"
[ -n "$ips" ] || die '域名解析没有返回 IP,未修改 PF'
mkdir -p /etc/pf.anchors "$BACKUP_DIR"
backup="$BACKUP_DIR/pf.conf.$(date +%Y%m%d-%H%M%S)"
cp -p "$PF_CONF" "$backup"
{
printf 'table <wechat_update> persist { '
printf '%s' "$ips" | tr '\n' ' '
printf '}\nblock drop out quick to <wechat_update>\n'
} > "$ANCHOR_FILE"
if ! grep -Fq 'anchor "com.tencent.wechat.update"' "$PF_CONF"; then
printf '\n# TraceMemo: block WeChat update endpoints\nanchor "com.tencent.wechat.update"\nload anchor "com.tencent.wechat.update" from "/etc/pf.anchors/wechat-update"\n' >> "$PF_CONF"
fi
pfctl -vnf "$PF_CONF"
pfctl -f "$PF_CONF"
pfctl -E >/dev/null 2>&1 || true
printf '已启用微信更新拦截,规则备份:%s\n' "$backup"
printf '当前 IP\n%s\n' "$ips"
}
disable() {
need_root
pfctl -a "$ANCHOR" -F all >/dev/null 2>&1 || true
printf '已清空微信更新 PF anchor。要完全移除配置行,请从 /etc/pf.conf 删除 TraceMemo 标记的三行。\n'
}
status() {
pfctl -s info 2>&1 | head -20
printf '\n微信更新 anchor\n'
pfctl -a "$ANCHOR" -sr 2>&1 || true
printf '\n缓存目录:\n%s\n' "$CACHE_DIR"
ls -ldO "$CACHE_DIR" 2>/dev/null || true
}
case "${1:-status}" in
enable) enable ;;
disable) disable ;;
status) status ;;
*) die "用法:sudo $0 {enable|disable|status}" ;;
esac
+225
View File
@@ -0,0 +1,225 @@
/*
* 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/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}`)
}
const pythonPackages = path.join(outputDir, 'python')
const pilkPackage = path.join(pythonPackages, 'pilk')
if (!fs.existsSync(pilkPackage)) {
console.log('[wechat-personal] 安装项目内语音编码器 pilk')
fs.mkdirSync(pythonPackages, { recursive: true })
try {
execFileSync(
'python3',
[
'-m',
'pip',
'install',
'--disable-pip-version-check',
'--no-compile',
'--target',
pythonPackages,
'pilk==0.2.4'
],
{ stdio: 'inherit' }
)
} catch {
console.warn('[wechat-personal] pilk 安装失败,将使用 OneBot 内置的 Go SILK 编码器')
}
}
function patchPerSendPayload(scriptPath) {
let source = fs.readFileSync(scriptPath, 'utf8')
if (source.includes('var activeTriggerX1Payload = ptr(0);')) return
const declarations = 'var triggerX1Payload;\nvar triggerX0;'
const patchedDeclarations =
'var triggerX1Payload;\nvar activeTriggerX1Payload = ptr(0);\nvar triggerX0;'
const originalSend = ` 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));`
const patchedSend = ` 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));`
if (!source.includes(declarations) || !source.includes(originalSend)) {
throw new Error('无法定位 wechat_chatter 连续发送补丁位置')
}
source = source.replace(declarations, patchedDeclarations).replace(originalSend, patchedSend)
source = source.replace(
' MMStartTask(triggerX0, triggerX1Payload);',
' MMStartTask(triggerX0, activeTriggerX1Payload);'
)
source = source.replace(
' } catch (e) {\n console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);',
' } catch (e) {\n activeTriggerX1Payload = ptr(0);\n console.error("[!] Error trigger " + msgType + " MMStartTask: " + e);'
)
source = source.replace(
'\t\t\t\tpendingSendMsgType = "";\n\t\t\t\treturn',
'\t\t\t\tpendingSendMsgType = "";\n\t\t\t\tactiveTriggerX1Payload = ptr(0);\n\t\t\t\treturn'
)
fs.writeFileSync(scriptPath, source)
console.log('[wechat-personal] 已应用逐条发送 payload 隔离补丁')
}
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 状态补丁')
}
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, 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)
patchImageHookReadiness(script)
addModifiedWorkNotice(script)
fs.chmodSync(executable, 0o755)
console.log('[wechat-personal] 运行时准备完成')