/** * android-wechat-sender.js — 手机端微信消息发送(无 adb,直接 su) * * 运行环境:Pixel 10 Pro Fold 的 Termux / Debian chroot * 前提:已 root,LSPosed 模块已启用,微信主进程运行中 * * 原理:su -c "content insert ..." → 模块 Provider → 广播 → 微信内部 API * 或 su -c "am broadcast -a com.hermes.wechat.SEND_MSG --es toUser ..." */ const { spawn } = require('child_process'); const PROVIDER_URI = 'content://com.hermes.wechat.xposed.provider/send'; const STATUS_URI = 'content://com.hermes.wechat.xposed.provider/status'; const ACTION_SEND = 'com.hermes.wechat.SEND_MSG'; /** * 检测当前是否已有 root 权限(uid=0) */ function hasRoot() { return process.getuid && process.getuid() === 0; } /** * 包装命令:非 root 时自动加 su -c */ function wrapCmd(cmd) { if (hasRoot()) return cmd; // su -c 的参数需要整体作为一个字符串传给 su return `su -c "${cmd.replace(/"/g, '\\"')}"`; } /** * 执行 shell 命令,返回 Promise * @param {string} cmd — 原始命令(不含 su) * @param {number} timeoutMs */ function shell(cmd, timeoutMs = 15000) { const fullCmd = wrapCmd(cmd); const args = ['-c', fullCmd]; // Debian chroot 中可能用 /bin/sh,Termux 中也有 const shellBin = process.env.SHELL || '/system/bin/sh'; return new Promise((resolve, reject) => { const proc = spawn(shellBin, args, { timeout: timeoutMs }); let stdout = ''; let stderr = ''; proc.stdout.on('data', d => stdout += d.toString()); proc.stderr.on('data', d => stderr += d.toString()); proc.on('close', code => { if (code !== 0) { reject(new Error(`exit ${code}: ${stderr.trim() || stdout.trim()}`)); } else { resolve(stdout.trim()); } }); proc.on('error', reject); }); } /** * 转义单引号(shell 单引号内安全) * "it's" → "it'\''s" */ function escapeShellSingleQuote(str) { return str.replace(/'/g, "'\\''"); } /** * 发送文本消息(通过 ContentProvider) * @param {string} toUser — wxid / filehelper / 群聊ID * @param {string} content — 消息内容 * @returns {Promise<{ok: boolean, uri?: string, error?: string}>} */ async function sendTextMessage(toUser, content) { if (!toUser || !content) { return { ok: false, error: 'toUser and content are required' }; } const safeToUser = escapeShellSingleQuote(toUser); const safeContent = escapeShellSingleQuote(content); const cmd = `content insert --uri ${PROVIDER_URI} --bind toUser:s:'${safeToUser}' --bind content:s:'${safeContent}'`; try { const out = await shell(cmd, 10000); if (out.includes(PROVIDER_URI)) { return { ok: true, uri: out }; } return { ok: false, error: `unexpected output: ${out}` }; } catch (e) { return { ok: false, error: e.message }; } } /** * 发送文本消息(通过广播,更直接,绕过 ContentProvider 进程) * @param {string} toUser * @param {string} content * @returns {Promise<{ok: boolean, error?: string}>} */ async function sendTextMessageBroadcast(toUser, content) { if (!toUser || !content) { return { ok: false, error: 'toUser and content are required' }; } const safeToUser = escapeShellSingleQuote(toUser); const safeContent = escapeShellSingleQuote(content); const cmd = `am broadcast -a ${ACTION_SEND} --es toUser '${safeToUser}' --es content '${safeContent}'`; try { const out = await shell(cmd, 10000); // am broadcast 成功输出通常包含 "Broadcast completed: result=0" if (out.includes('Broadcast completed') || out.includes('result=')) { return { ok: true }; } return { ok: false, error: `unexpected output: ${out}` }; } catch (e) { return { ok: false, error: e.message }; } } /** * 查询最近一次发送结果 * @returns {Promise<{status: string, error?: string}>} */ async function queryStatus() { try { const out = await shell(`content query --uri ${STATUS_URI}`, 5000); const m = out.match(/status=(\w+)/); return { status: m ? m[1] : 'unknown' }; } catch (e) { return { status: 'error', error: e.message }; } } /** * 批量发送(带间隔,避免被风控) * @param {Array<{toUser: string, content: string}>} messages * @param {number} delayMs — 默认 2000ms */ async function sendBatch(messages, delayMs = 2000) { const results = []; for (const msg of messages) { const r = await sendTextMessage(msg.toUser, msg.content); results.push({ ...msg, ...r }); if (delayMs > 0) await sleep(delayMs); } return results; } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } module.exports = { sendTextMessage, sendTextMessageBroadcast, queryStatus, sendBatch, shell, };