wechat-sender.js 3.73 KB
/**
 * wechat-sender.js — 通过 LSPosed 模块发送微信消息
 *
 * 依赖:手机已安装 LSPosed 模块 + HermesWeChatSendMsg + 微信作用域已启用
 * 原理:adb shell content insert → 模块 Provider → 广播 → WeChat 内部 API
 */

const { spawn } = require('child_process');
const path = require('path');

// adb 路径:优先使用项目内平台工具,否则从 PATH 查找
const ADB = process.env.ADB_PATH || (() => {
    try {
        // 尝试使用 lemonPCRM 项目里的 adb
        const projectAdb = path.resolve(__dirname, '../../../../platform-tools/adb');
        require('fs').accessSync(projectAdb, require('fs').constants.X_OK);
        return projectAdb;
    } catch {
        return 'adb';
    }
})();

const PROVIDER_URI = 'content://com.hermes.wechat.xposed.provider/send';
const STATUS_URI = 'content://com.hermes.wechat.xposed.provider/status';

/**
 * 执行 adb shell 命令,返回 Promise<string>
 */
function adbShell(cmd, timeoutMs = 15000) {
    return new Promise((resolve, reject) => {
        const args = cmd.startsWith('shell ') ? cmd.split(' ') : ['shell', ...cmd.split(' ')];
        const proc = spawn(ADB, 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(`adb exit ${code}: ${stderr.trim() || stdout.trim()}`));
            } else {
                resolve(stdout.trim());
            }
        });
        proc.on('error', reject);
    });
}

/**
 * 发送文本消息
 * @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' };
    }
    // 转义单引号防止 shell 注入
    const safeToUser = toUser.replace(/'/g, "'\\''");
    const safeContent = content.replace(/'/g, "'\\''");

    const cmd = `content insert --uri ${PROVIDER_URI} --bind toUser:s:'${safeToUser}' --bind content:s:'${safeContent}'`;

    try {
        const out = await adbShell(cmd, 10000);
        // adb content insert 成功时输出类似:content://com.hermes.wechat.xposed.provider/send/0
        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 };
    }
}

/**
 * 查询最近一次发送结果(简单轮询用)
 * @returns {Promise<{status: string}>}
 */
async function queryStatus() {
    try {
        const out = await adbShell(`content query --uri ${STATUS_URI}`, 5000);
        // 输出格式:Row: status=pending / true / false
        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,
    queryStatus,
    sendBatch,
    adbShell,
    ADB,
};