android-wechat-sender.js 4.7 KB
/**
 * 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<string>
 * @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,
};