4009 lines
174 KiB
JavaScript
4009 lines
174 KiB
JavaScript
/**
|
||
* wechat_hook_v2.js — 微信 Frida Hook 脚本(机擎 SDK v3.0)
|
||
*
|
||
* 全功能微信控制引擎 — 110 个操作 / 24 模块
|
||
*
|
||
* 模块清单:
|
||
* H15 消息接收(SQLite insert Hook)
|
||
* H16 联系人获取(DB 直读)
|
||
* H17 消息发送(内部方法 / Intent 降级)
|
||
* H18 好友请求监听(SQLite + BroadcastReceiver)
|
||
* H19 添加/通过好友(XmlParser Hook + Intent)
|
||
* H20 朋友圈发布(SnsService Hook + Intent)
|
||
* H21 朋友圈浏览/点赞(DB + RPC)
|
||
* H22 群管理(创建/邀请/踢人/公告)
|
||
* H23 账号管理(昵称/头像/签名/状态/性别/地区)
|
||
* H24 账号安全(解封/改密/绑定手机/登录设备管理)
|
||
* H25 支付(红包/转账/收款/零钱)
|
||
* H26 二维码(扫码/生成个人码/群码)
|
||
* H27 视频号(浏览/点赞/评论/关注)
|
||
* H28 标签管理(创建/删除/打标/查询)
|
||
* H29 收藏管理(添加/删除/列表)
|
||
* H30 设置(隐私/通知/通用/聊天背景)
|
||
* H31 搜索(全局搜索/联系人/消息/公众号)
|
||
* H32 小程序(打开/最近/收藏)
|
||
* H33 文件传输(发送文件/图片/视频/语音)
|
||
* H34 消息转发(单条/合并转发)
|
||
* H35 注册/登录(新号注册/登录/切号)
|
||
* H36 公众号(关注/取关/列表/文章)
|
||
* H37 表情管理(发送/收藏/自定义)
|
||
* H38 浮窗/多任务
|
||
* H39 设备信息
|
||
*
|
||
* 技术路线:Frida 17.x + Java.perform + SQLite Hook + rpc.exports
|
||
*
|
||
* @version 3.1.0 (版本自适应)
|
||
*/
|
||
'use strict';
|
||
|
||
var PLATFORM = 'wechat';
|
||
|
||
var CONFIG = {
|
||
WECHAT_PACKAGE: 'com.tencent.mm',
|
||
LOG_LEVEL: 'info',
|
||
DB_NAME: 'EnMicroMsg.db',
|
||
SNS_DB_NAME: 'SnsMicroMsg.db',
|
||
HOOK_SQLITE: true,
|
||
HOOK_NETWORK: true,
|
||
HOOK_FRIEND_REQUEST: true,
|
||
HOOK_MOMENTS: true,
|
||
MESSAGE_BUFFER_MAX: 500,
|
||
FRIEND_REQUEST_BUFFER_MAX: 100,
|
||
};
|
||
|
||
// ============================================================
|
||
// § 0 版本兼容系统 (H24)
|
||
// ============================================================
|
||
|
||
var _versionCompat = null;
|
||
|
||
var _VERSION_TABLE = {
|
||
'8.0.44': {
|
||
send_message: [
|
||
{ cls: 'com.tencent.mm.modelmulti.h', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] }
|
||
],
|
||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.b' }],
|
||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||
},
|
||
'8.0.49': {
|
||
send_message: [
|
||
{ cls: 'com.tencent.mm.modelmulti.g', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] },
|
||
{ cls: 'com.tencent.mm.modelmulti.h', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] }
|
||
],
|
||
friend_request: [
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' },
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.b' }
|
||
],
|
||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||
},
|
||
'8.0.51': {
|
||
send_message: [
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.k', method: 'a', sig: ['java.lang.String', 'java.lang.String'] },
|
||
{ cls: 'com.tencent.mm.modelmulti.g', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] }
|
||
],
|
||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }],
|
||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||
},
|
||
'8.0.56': {
|
||
send_message: [
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] },
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.k', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }
|
||
],
|
||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }],
|
||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }, { cls: 'com.tencent.mm.plugin.sns.ui.SnsTimeLineUI' }],
|
||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||
},
|
||
'8.0.58': {
|
||
send_message: [
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }
|
||
],
|
||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }],
|
||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||
},
|
||
'8.0.60': {
|
||
send_message: [
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }
|
||
],
|
||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }],
|
||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||
},
|
||
'8.0.69': {
|
||
send_message: [
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }
|
||
],
|
||
friend_request: [{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }],
|
||
sns_upload: [{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' }],
|
||
add_friend: [{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' }]
|
||
}
|
||
};
|
||
|
||
var _DEFAULT_COMPAT = {
|
||
send_message: [
|
||
{ cls: 'com.tencent.mm.modelmulti.h', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] },
|
||
{ cls: 'com.tencent.mm.modelmulti.g', method: 'b', sig: ['java.lang.String', 'java.lang.String', 'java.lang.String'] },
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.k', method: 'a', sig: ['java.lang.String', 'java.lang.String'] },
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.j', method: 'a', sig: ['java.lang.String', 'java.lang.String'] }
|
||
],
|
||
friend_request: [
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.b' },
|
||
{ cls: 'com.tencent.mm.plugin.messenger.foundation.a.c' }
|
||
],
|
||
sns_upload: [
|
||
{ cls: 'com.tencent.mm.plugin.sns.ui.SnsUploadUI' },
|
||
{ cls: 'com.tencent.mm.plugin.sns.ui.SnsTimeLineUI' }
|
||
],
|
||
add_friend: [
|
||
{ cls: 'com.tencent.mm.plugin.profile.ui.ContactInfoUI' },
|
||
{ cls: 'com.tencent.mm.protocal.protobuf.add$ContactType' }
|
||
]
|
||
};
|
||
|
||
function _parseVersion(v) {
|
||
var parts = (v || '').split('.').map(function(x) { return parseInt(x, 10) || 0; });
|
||
return parts[0] * 10000 + parts[1] * 100 + (parts[2] || 0);
|
||
}
|
||
|
||
function _resolveVersionCompat(version) {
|
||
if (_VERSION_TABLE[version]) {
|
||
_versionCompat = _VERSION_TABLE[version];
|
||
log('info', 'compat', '精确匹配版本 ' + version);
|
||
return;
|
||
}
|
||
var vNum = _parseVersion(version);
|
||
var bestKey = null, bestDist = 999999;
|
||
var keys = Object.keys(_VERSION_TABLE);
|
||
for (var i = 0; i < keys.length; i++) {
|
||
var kNum = _parseVersion(keys[i]);
|
||
var dist = Math.abs(vNum - kNum);
|
||
if (dist < bestDist) { bestDist = dist; bestKey = keys[i]; }
|
||
}
|
||
if (bestKey && bestDist <= 10) {
|
||
_versionCompat = _VERSION_TABLE[bestKey];
|
||
log('info', 'compat', '近似匹配 ' + version + ' → ' + bestKey + ' (距离=' + bestDist + ')');
|
||
} else {
|
||
_versionCompat = _DEFAULT_COMPAT;
|
||
log('warn', 'compat', '未匹配版本 ' + version + ',使用 default 全候选');
|
||
}
|
||
}
|
||
|
||
function getCompat(key) {
|
||
return (_versionCompat && _versionCompat[key]) || (_DEFAULT_COMPAT[key]) || [];
|
||
}
|
||
|
||
// ============================================================
|
||
// § 1 工具函数
|
||
// ============================================================
|
||
|
||
var LOG_LEVEL_MAP = { debug: 0, info: 1, warn: 2, error: 3 };
|
||
|
||
function log(level, tag, message, extra) {
|
||
if ((LOG_LEVEL_MAP[level] || 0) < (LOG_LEVEL_MAP[CONFIG.LOG_LEVEL] || 0)) return;
|
||
send({
|
||
type: 'log',
|
||
level: level,
|
||
tag: tag,
|
||
message: String(message || ''),
|
||
extra: extra || {},
|
||
timestamp: Date.now(),
|
||
});
|
||
}
|
||
|
||
function emitEvent(eventType, payload) {
|
||
send({
|
||
type: 'hook_event',
|
||
event_type: eventType,
|
||
platform: PLATFORM,
|
||
payload: payload || {},
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
|
||
function safeStr(v) {
|
||
if (v === null || v === undefined) return '';
|
||
try { return v.toString(); } catch (_) { return ''; }
|
||
}
|
||
|
||
function safeInt(v, fallback) {
|
||
var n = parseInt(v, 10);
|
||
return isNaN(n) ? (fallback || 0) : n;
|
||
}
|
||
|
||
function jStr(s) {
|
||
return Java.use('java.lang.String').$new(String(s));
|
||
}
|
||
|
||
function jLong(v) {
|
||
var Long = Java.use('java.lang.Long');
|
||
var s = String(v === undefined || v === null ? '0' : v);
|
||
try {
|
||
return Long.parseLong(s);
|
||
} catch (_) {
|
||
return Long.valueOf(parseInt(s, 10) || 0);
|
||
}
|
||
}
|
||
|
||
function getCtx() {
|
||
return Java.use('android.app.ActivityThread').currentApplication().getApplicationContext();
|
||
}
|
||
|
||
function generateId(prefix) {
|
||
return (prefix || 'wh') + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6);
|
||
}
|
||
|
||
// ============================================================
|
||
// § 2 全局状态
|
||
// ============================================================
|
||
|
||
var _messageBuffer = [];
|
||
var _friendRequestBuffer = [];
|
||
var _contactCache = [];
|
||
var _dbPath = null;
|
||
var _snsDbPath = null;
|
||
var _wechatVersion = '';
|
||
var _hooked = {
|
||
sqlite_insert: false,
|
||
sqlite_update: false,
|
||
network: false,
|
||
friend_request: false,
|
||
sns_upload: false,
|
||
};
|
||
|
||
// ============================================================
|
||
// § 3 数据库通用层
|
||
// ============================================================
|
||
|
||
function _findDbPath(dbName) {
|
||
try {
|
||
var File = Java.use('java.io.File');
|
||
var dir = File.$new('/data/data/com.tencent.mm/MicroMsg/');
|
||
if (!dir.exists()) return null;
|
||
var subs = dir.listFiles();
|
||
if (!subs) return null;
|
||
for (var i = 0; i < subs.length; i++) {
|
||
var p = subs[i].getAbsolutePath() + '/' + dbName;
|
||
if (File.$new(p).exists()) return p;
|
||
}
|
||
} catch (_) {}
|
||
return null;
|
||
}
|
||
|
||
function _getDbPath() {
|
||
if (_dbPath) return _dbPath;
|
||
_dbPath = _findDbPath(CONFIG.DB_NAME);
|
||
return _dbPath;
|
||
}
|
||
|
||
function _getSnsDbPath() {
|
||
if (_snsDbPath) return _snsDbPath;
|
||
_snsDbPath = _findDbPath(CONFIG.SNS_DB_NAME);
|
||
return _snsDbPath;
|
||
}
|
||
|
||
var _lastDbDiag = { ok: false, error: '', path: '', found: false, rows: 0 };
|
||
|
||
function _emptyReadFailure(kind, diag, force) {
|
||
var d = diag || _lastDbDiag || {};
|
||
var hasDbError = d && (d.ok === false) && (d.error || d.found === false);
|
||
if (!hasDbError && !force) return null;
|
||
return {
|
||
success: false,
|
||
error: kind + '读取为空:DB 未打开或查询不稳定',
|
||
count: 0,
|
||
_diag: d,
|
||
};
|
||
}
|
||
|
||
function _execSQL(sql, dbPathOverride) {
|
||
var results = [];
|
||
_lastDbDiag = { ok: false, error: '', path: '', found: false, rows: 0 };
|
||
try {
|
||
var path = dbPathOverride || _getDbPath();
|
||
_lastDbDiag.path = path || '';
|
||
if (!path) { _lastDbDiag.error = 'db_path 未找到'; return results; }
|
||
var db = _findOpenWcdb(path);
|
||
if (!db) {
|
||
_lastDbDiag.error = '未找到已打开的 WCDB 实例';
|
||
log('warn', 'db', '未找到已打开的 WCDB 实例: ' + path);
|
||
return results;
|
||
}
|
||
_lastDbDiag.found = true;
|
||
// WCDB 1.x: rawQuery(sql, args);2.x core.Database 无 rawQuery → 走 2.x 适配
|
||
if (typeof db.rawQuery !== 'function') {
|
||
return _execSQL2x(db, sql, results);
|
||
}
|
||
var cursor = db.rawQuery(sql, null);
|
||
while (cursor.moveToNext()) {
|
||
var row = {};
|
||
for (var c = 0; c < cursor.getColumnCount(); c++) {
|
||
row[cursor.getColumnName(c)] = safeStr(cursor.getString(c));
|
||
}
|
||
results.push(row);
|
||
}
|
||
cursor.close();
|
||
_lastDbDiag.ok = true;
|
||
_lastDbDiag.rows = results.length;
|
||
} catch (e) {
|
||
_lastDbDiag.error = String(e);
|
||
log('warn', 'db', 'SQL 执行失败: ' + e);
|
||
}
|
||
return results;
|
||
}
|
||
|
||
// WCDB 2.x core.Database 原生 SQL 查询适配。
|
||
// 2.x 无 rawQuery;通过 getHandle() 取 Handle,再用 PreparedStatement(prepare/step/getText)读取。
|
||
// 反射发现可用方法,失败则把方法名写入 _lastDbDiag 供下一轮精确实现(不盲写)。
|
||
function _execSQL2x(db, sql, results) {
|
||
try {
|
||
if (typeof db.getHandle !== 'function') {
|
||
_lastDbDiag.error = 'WCDB2x: 实例无 getHandle()';
|
||
try { _lastDbDiag.db_methods = _reflectMethodNames(db); } catch (_) {}
|
||
return results;
|
||
}
|
||
var handle = db.getHandle();
|
||
_lastDbDiag.has_handle = !!handle;
|
||
if (!handle || typeof handle.getAllRowsFromSQL !== 'function') {
|
||
_lastDbDiag.error = 'WCDB2x: handle 无 getAllRowsFromSQL';
|
||
try { _lastDbDiag.handle_methods = _reflectMethodNames(handle); } catch (_) {}
|
||
return results;
|
||
}
|
||
// 从 SELECT 子句解析列名(getAllRowsFromSQL 只返回值数组,需自带列名映射)
|
||
var colNames = _parseSelectColumns(sql);
|
||
_lastDbDiag.used_prepare = 'getAllRowsFromSQL';
|
||
_lastDbDiag.parsed_cols = colNames;
|
||
// WCDB 2.x:getAllRowsFromSQL(sql) → List<List<Value>>(Value.getText() 取文本)
|
||
var allRows = handle.getAllRowsFromSQL(sql);
|
||
if (allRows) {
|
||
var rowCount = (typeof allRows.size === 'function') ? allRows.size() : allRows.length;
|
||
for (var ri = 0; ri < rowCount && ri < 2000; ri++) {
|
||
var rowVals = (typeof allRows.get === 'function') ? allRows.get(ri) : allRows[ri];
|
||
var vcount = (rowVals && typeof rowVals.size === 'function') ? rowVals.size()
|
||
: (rowVals && rowVals.length != null ? rowVals.length : 0);
|
||
var row = {};
|
||
for (var vi = 0; vi < vcount; vi++) {
|
||
var v = (typeof rowVals.get === 'function') ? rowVals.get(vi) : rowVals[vi];
|
||
var txt = null;
|
||
try { if (v && typeof v.getText === 'function') txt = v.getText(); } catch (_) {}
|
||
if (txt == null) { try { if (v != null) txt = String(v); } catch (_) {} }
|
||
row[colNames[vi] || ('c' + vi)] = safeStr(txt);
|
||
}
|
||
results.push(row);
|
||
}
|
||
}
|
||
try { if (handle && typeof handle.invalidate === 'function') handle.invalidate(); } catch (_) {}
|
||
_lastDbDiag.ok = true;
|
||
_lastDbDiag.rows = results.length;
|
||
} catch (e) {
|
||
_lastDbDiag.error = 'WCDB2x: ' + String(e);
|
||
log('warn', 'db', _lastDbDiag.error);
|
||
}
|
||
return results;
|
||
}
|
||
|
||
// 从 SELECT ... FROM 解析列名/别名,供 getAllRowsFromSQL 的值数组按名映射。
|
||
// 仅处理本文件内构造的受控 SQL(无嵌套子查询函数逗号);尽量取 AS 别名或末段标识符。
|
||
function _parseSelectColumns(sql) {
|
||
try {
|
||
// PRAGMA table_info(X) 列固定,getAllRowsFromSQL 不带列名,需手动映射
|
||
if (/^\s*pragma\s+table_info/i.test(sql)) {
|
||
return ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'];
|
||
}
|
||
var m = /select\s+([\s\S]*?)\s+from\s/i.exec(sql);
|
||
if (!m) return [];
|
||
var seg = m[1];
|
||
if (seg.replace(/\s/g, '') === '*') return [];
|
||
return seg.split(',').map(function (part) {
|
||
var p = part.trim();
|
||
var asm = /\s+as\s+([A-Za-z0-9_]+)\s*$/i.exec(p);
|
||
if (asm) return asm[1];
|
||
// count(*) AS n 已被上分支处理;无别名时取末段标识符
|
||
var idm = /([A-Za-z0-9_]+)\s*$/.exec(p);
|
||
return idm ? idm[1] : p;
|
||
});
|
||
} catch (_) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function _reflectMethodNames(obj) {
|
||
var names = [];
|
||
try {
|
||
var cls = obj.getClass();
|
||
var methods = cls.getMethods();
|
||
for (var i = 0; i < methods.length && i < 120; i++) {
|
||
try { names.push(methods[i].getName()); } catch (_) {}
|
||
}
|
||
} catch (_) {}
|
||
// 去重
|
||
var seen = {}, uniq = [];
|
||
for (var j = 0; j < names.length; j++) { if (!seen[names[j]]) { seen[names[j]] = 1; uniq.push(names[j]); } }
|
||
return uniq;
|
||
}
|
||
|
||
function _reflectMethodSigs(obj, limit) {
|
||
var sigs = [];
|
||
try {
|
||
var cls = obj.getClass();
|
||
var methods = cls.getMethods();
|
||
var max = limit || 120;
|
||
for (var i = 0; i < methods.length && i < max; i++) {
|
||
try { sigs.push(safeStr(methods[i].toString())); } catch (_) {}
|
||
}
|
||
} catch (_) {}
|
||
return sigs;
|
||
}
|
||
|
||
function _escapeSql(v) {
|
||
return String(v == null ? '' : v).replace(/'/g, "''");
|
||
}
|
||
|
||
/** WCDB 1.x 写库(标签/联系人字段);无 execSQL 时诚实失败 */
|
||
function _execSQLWrite(sql, dbPathOverride) {
|
||
try {
|
||
var path = dbPathOverride || _getDbPath();
|
||
if (!path) return { success: false, error: 'db_path 未找到' };
|
||
var db = _findOpenWcdb(path);
|
||
if (!db) return { success: false, error: '未找到已打开的 WCDB 实例' };
|
||
if (typeof db.execSQL !== 'function') {
|
||
return { success: false, error: '实例无 execSQL(疑 WCDB 2.x),标签写库不可用' };
|
||
}
|
||
db.execSQL(sql);
|
||
return { success: true };
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
}
|
||
|
||
function _nextLabelId() {
|
||
var rows = _execSQL('SELECT MAX(CAST(labelID AS INTEGER)) AS max_id FROM contactlabel');
|
||
if (rows.length && rows[0].max_id) {
|
||
var n = parseInt(rows[0].max_id, 10);
|
||
if (!isNaN(n)) return n + 1;
|
||
}
|
||
rows = _execSQL('SELECT MAX(CAST(labelId AS INTEGER)) AS max_id FROM contactlabelinfo');
|
||
if (rows.length && rows[0].max_id) {
|
||
var n2 = parseInt(rows[0].max_id, 10);
|
||
if (!isNaN(n2)) return n2 + 1;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
function _normalizeLabelIds(raw) {
|
||
if (!raw) return [];
|
||
if (Array.isArray(raw)) return raw.map(function (x) { return String(x); }).filter(Boolean);
|
||
return String(raw).split(',').map(function (x) { return x.trim(); }).filter(Boolean);
|
||
}
|
||
|
||
function _tryNotifyLabelChanged() {
|
||
try {
|
||
var candidates = [
|
||
'com.tencent.mm.plugin.label.a.a',
|
||
'com.tencent.mm.plugin.label.a.b',
|
||
'com.tencent.mm.storage.bv',
|
||
];
|
||
for (var i = 0; i < candidates.length; i++) {
|
||
try {
|
||
var Cls = Java.use(candidates[i]);
|
||
if (typeof Cls.a === 'function') { Cls.a(); return true; }
|
||
if (typeof Cls.b === 'function') { Cls.b(); return true; }
|
||
} catch (_) {}
|
||
}
|
||
} catch (_) {}
|
||
return false;
|
||
}
|
||
|
||
// WCDB 实例查找(兼容 8.0.69 WCDB 2.x):多候选类名 + 放宽路径匹配
|
||
var _WCDB_CLASS_CANDIDATES = [
|
||
'com.tencent.wcdb.database.SQLiteDatabase', // WCDB 1.x
|
||
'com.tencent.wcdb.core.Database', // WCDB 2.x(新版微信)
|
||
];
|
||
var _wcdbClassResolved = null;
|
||
|
||
function _wcdbInstancePath(instance) {
|
||
// WCDB 1.x: getPath();2.x: getPath() 或 path 字段
|
||
try { if (typeof instance.getPath === 'function') return safeStr(instance.getPath()); } catch (_) {}
|
||
try { if (instance.path) return safeStr(instance.path.value); } catch (_) {}
|
||
return '';
|
||
}
|
||
|
||
function _wcdbIsOpen(instance) {
|
||
try { if (typeof instance.isOpen === 'function') return instance.isOpen(); } catch (_) {}
|
||
try { if (typeof instance.isOpened === 'function') return instance.isOpened(); } catch (_) {}
|
||
return true; // 2.x 无显式 isOpen 时按打开处理
|
||
}
|
||
|
||
// WCDB 实例缓存:Java.choose 全堆扫描在大堆上耗时数十秒,标签读写每次都扫会超时。
|
||
// 按 path 缓存已打开实例;命中先校验 isOpen,失效则重扫。显著降低标签操作延迟。
|
||
var _wcdbInstCache = {};
|
||
|
||
function _findOpenWcdb(path) {
|
||
var normalizedPath = path.replace('/data/data/', '/data/user/0/');
|
||
var baseName = path.split('/').pop();
|
||
var found = null;
|
||
// 1) 先查缓存:命中且仍打开则直接复用,避免全堆扫描
|
||
var cached = _wcdbInstCache[path];
|
||
if (cached) {
|
||
try {
|
||
if (_wcdbIsOpen(cached)) return cached;
|
||
} catch (_) {}
|
||
try { delete _wcdbInstCache[path]; } catch (_) { _wcdbInstCache[path] = null; }
|
||
}
|
||
// 始终遍历全部候选类:不同库分属不同 WCDB 版本(EnMicroMsg=1.x,SnsMicroMsg=2.x),
|
||
// 不能用单一 _wcdbClassResolved 缓存限制,否则跨版本库找不到。
|
||
var classes = _WCDB_CLASS_CANDIDATES;
|
||
for (var ci = 0; ci < classes.length && !found; ci++) {
|
||
var cls = classes[ci];
|
||
try { Java.use(cls); } catch (_) { continue; } // 类不存在则跳过
|
||
try {
|
||
Java.choose(cls, {
|
||
onMatch: function (instance) {
|
||
if (found) return;
|
||
try {
|
||
var p = _wcdbInstancePath(instance);
|
||
var match = (p === path || p === normalizedPath || (baseName && p.indexOf(baseName) >= 0));
|
||
if (match && _wcdbIsOpen(instance)) {
|
||
found = instance;
|
||
_wcdbClassResolved = cls;
|
||
_wcdbInstCache[path] = instance; // 缓存供后续复用
|
||
}
|
||
} catch (_) {}
|
||
},
|
||
onComplete: function () {}
|
||
});
|
||
} catch (e) {
|
||
log('warn', 'db', 'Java.choose 失败 ' + cls + ': ' + e);
|
||
}
|
||
}
|
||
if (!found) log('warn', 'db', '未找到已打开的 WCDB 实例(已试' + classes.length + '类): ' + baseName);
|
||
return found;
|
||
}
|
||
|
||
// 诊断:枚举所有 WCDB 实例路径(真机排查 8.0.69 DB 读空)
|
||
function _diagWcdbInstances() {
|
||
var out = [];
|
||
for (var ci = 0; ci < _WCDB_CLASS_CANDIDATES.length; ci++) {
|
||
var cls = _WCDB_CLASS_CANDIDATES[ci];
|
||
try { Java.use(cls); } catch (_) { out.push({cls: cls, exists: false}); continue; }
|
||
var paths = [];
|
||
try {
|
||
Java.choose(cls, {
|
||
onMatch: function (inst) {
|
||
try { paths.push({ path: _wcdbInstancePath(inst), open: _wcdbIsOpen(inst) }); } catch (_) {}
|
||
},
|
||
onComplete: function () {}
|
||
});
|
||
} catch (_) {}
|
||
out.push({ cls: cls, exists: true, instances: paths });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function _execSnsSQL(sql) {
|
||
return _execSQL(sql, _getSnsDbPath());
|
||
}
|
||
|
||
// ============================================================
|
||
// § 4 rpc.exports — Python 端通过 FridaManager.call_rpc 调用
|
||
// ============================================================
|
||
|
||
rpc.exports = {
|
||
|
||
// ==================== 系统 ====================
|
||
|
||
ping: function () {
|
||
return 'pong from wechat_hook_v3.0 — 96 actions / 26 modules';
|
||
},
|
||
|
||
getProcessInfo: function () {
|
||
return {
|
||
pid: Process.id,
|
||
arch: Process.arch,
|
||
platform: Process.platform,
|
||
modules: Process.enumerateModules().length,
|
||
wechat_version: _wechatVersion,
|
||
hooked: _hooked,
|
||
};
|
||
},
|
||
|
||
getHookStatus: function () {
|
||
return _hooked;
|
||
},
|
||
|
||
// 诊断:枚举真机 WCDB 实例路径(排查 8.0.69 DB 读空)
|
||
diagWcdb: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var out = { success: true, db_path: _getDbPath(), sns_path: _getSnsDbPath(), instances: _diagWcdbInstances() };
|
||
// 实读测试:main 库 rcontact、sns 库 SnsInfo,回传 _lastDbDiag 定位查询错误
|
||
try { _execSQL('SELECT count(*) AS n FROM rcontact'); out.main_query = JSON.parse(JSON.stringify(_lastDbDiag)); } catch (e1) { out.main_query = { error: String(e1) }; }
|
||
try { _execSnsSQL('SELECT count(*) AS n FROM SnsInfo'); out.sns_query = JSON.parse(JSON.stringify(_lastDbDiag)); } catch (e2) { out.sns_query = { error: String(e2) }; }
|
||
return out;
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
// 诊断:运行时反射枚举 8.0.69 朋友圈(SNS)发布/读取相关类与方法
|
||
// 用于在混淆版定位 post_moments(sns.model.ay 等价) / get_moments(m3.onGYNetEnd 等价) 真实类名。
|
||
diagSns: function (params) {
|
||
// 安全诊断:**禁用** Java.enumerateLoadedClassesSync(微信 10万+ 类,桥接枚举会阻塞
|
||
// agent 事件循环导致 WS 掉线 — 2026-05-30 实测)。改为「试探调用方提供的候选类名」:
|
||
// params.candidates = ["com.tencent.mm.plugin.sns.model.xx", ...](来自离线 apk/smali 分析)
|
||
// 对每个候选 Java.use + 反射方法名(最多 8 个,快速、不枚举),定位发/读朋友圈真实类。
|
||
var cands = (params && params.candidates) || [];
|
||
if (!cands || cands.length === 0) {
|
||
return {
|
||
success: false,
|
||
error: 'no_candidates',
|
||
hint: '运行时全量枚举已禁用(会致 WS 掉线)。请先离线对 8.0.69 apk 跑 jadx/baksmali 取候选类名,再传 params.candidates=[...] 试探。',
|
||
};
|
||
}
|
||
var lim = Math.min(cands.length, 8);
|
||
try {
|
||
return Java.performNow(function () {
|
||
var out = [];
|
||
for (var i = 0; i < lim; i++) {
|
||
var cn = cands[i];
|
||
var info = { cls: cn, exists: false, methods: [], ctors: 0, hasCommit: false };
|
||
try {
|
||
var k = Java.use(cn);
|
||
info.exists = true;
|
||
var ms = k.class.getDeclaredMethods();
|
||
var names = {};
|
||
for (var m = 0; m < ms.length; m++) {
|
||
var mn = ms[m].getName();
|
||
names[mn] = 1;
|
||
if (mn.toLowerCase().indexOf('commit') >= 0) info.hasCommit = true;
|
||
}
|
||
info.methods = Object.keys(names).slice(0, 30);
|
||
info.ctors = k.class.getDeclaredConstructors().length;
|
||
} catch (e0) { info.err = String(e0); }
|
||
out.push(info);
|
||
}
|
||
return { success: true, tested: lim, classes: out };
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
getWechatVersion: function () {
|
||
if (_wechatVersion) return { success: true, version: _wechatVersion };
|
||
try {
|
||
return Java.performNow(function () {
|
||
var ctx = getCtx();
|
||
var pm = ctx.getPackageManager();
|
||
var info = pm.getPackageInfo(jStr('com.tencent.mm'), 0);
|
||
_wechatVersion = safeStr(info.versionName.value);
|
||
_resolveVersionCompat(_wechatVersion);
|
||
return { success: true, version: _wechatVersion };
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
getVersionCompat: function () {
|
||
return {
|
||
wechat_version: _wechatVersion,
|
||
matched: _versionCompat !== _DEFAULT_COMPAT,
|
||
supported_versions: Object.keys(_VERSION_TABLE),
|
||
active_compat: _versionCompat ? Object.keys(_versionCompat) : [],
|
||
};
|
||
},
|
||
|
||
diagSendMessageCandidates: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var candidates = getCompat('send_message');
|
||
var rows = [];
|
||
for (var i = 0; i < candidates.length; i++) {
|
||
var c = candidates[i];
|
||
var row = {
|
||
cls: c.cls,
|
||
method: c.method,
|
||
sig: c.sig || [],
|
||
class_found: false,
|
||
method_found: false,
|
||
overload_found: false,
|
||
error: ''
|
||
};
|
||
try {
|
||
var MsgCls = Java.use(c.cls);
|
||
row.class_found = !!MsgCls;
|
||
row.method_found = !!(MsgCls && MsgCls[c.method]);
|
||
if (row.method_found) {
|
||
if (c.sig && c.sig.length > 0) {
|
||
try {
|
||
MsgCls[c.method].overload.apply(MsgCls[c.method], c.sig);
|
||
row.overload_found = true;
|
||
} catch (oe) {
|
||
row.error = String(oe);
|
||
}
|
||
} else {
|
||
row.overload_found = true;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
row.error = String(e);
|
||
}
|
||
rows.push(row);
|
||
}
|
||
return {
|
||
success: true,
|
||
wechat_version: _wechatVersion,
|
||
candidate_count: rows.length,
|
||
candidates: rows
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
diagSendMsgEvent: function (params) {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var toId = (params && params.to_id) || 'filehelper';
|
||
var content = (params && params.content) || '[diag]';
|
||
var diag = {
|
||
success: true,
|
||
to_id: toId,
|
||
content_len: content.length,
|
||
classes: {},
|
||
field_set: false,
|
||
scene: null,
|
||
error: ''
|
||
};
|
||
try {
|
||
var SendMsgEvent = Java.use('com.tencent.mm.autogen.events.SendMsgEvent');
|
||
Java.use('com.tencent.mm.sdk.event.IEvent');
|
||
var event = SendMsgEvent.$new();
|
||
var payload = event.g.value;
|
||
payload.a.value = jStr(toId);
|
||
payload.b.value = jStr(content);
|
||
payload.c.value = _calcSendMsgScene(toId);
|
||
payload.d.value = 0;
|
||
diag.classes.SendMsgEvent = true;
|
||
diag.classes.payload = safeStr(payload.getClass().getName());
|
||
diag.event_methods = _reflectMethodSigs(event, 80);
|
||
diag.field_set = true;
|
||
diag.scene = payload.c.value;
|
||
} catch (inner) {
|
||
diag.success = false;
|
||
diag.error = String(inner);
|
||
}
|
||
return diag;
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
diagRevokeMsgEvent: function (params) {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||
var diag = { success: true, msg_svr_id: msgSvrId, classes: {}, payload_fields: [], row: null };
|
||
try {
|
||
if (msgSvrId) {
|
||
var _id = String(msgSvrId).replace(/'/g, "''");
|
||
var sql = "SELECT msgId, msgSvrId, talker, type, content, isSend, createTime FROM message"
|
||
+ " WHERE msgSvrId = '" + _id + "' OR msgId = '" + _id + "' LIMIT 1";
|
||
var rows = _execSQL(sql);
|
||
if (rows && rows.length > 0) diag.row = rows[0];
|
||
}
|
||
var RevokeMsgEvent = Java.use('com.tencent.mm.autogen.events.RevokeMsgEvent');
|
||
diag.classes.RevokeMsgEvent = true;
|
||
var event = RevokeMsgEvent.$new();
|
||
diag.event_methods = _reflectMethodSigs(event, 40);
|
||
var payload = event.g.value;
|
||
diag.payload_class = safeStr(payload.getClass().getName());
|
||
try {
|
||
var cls = payload.getClass();
|
||
var fields = cls.getDeclaredFields();
|
||
for (var i = 0; i < fields.length && i < 40; i++) {
|
||
fields[i].setAccessible(true);
|
||
var name = fields[i].getName();
|
||
var ftype = safeStr(fields[i].getType().getName());
|
||
var val = null;
|
||
try { val = fields[i].get(payload); } catch (_) { val = '?'; }
|
||
diag.payload_fields.push({ name: name, type: ftype, sample: safeStr(val) });
|
||
}
|
||
} catch (fe) {
|
||
diag.field_error = String(fe);
|
||
}
|
||
} catch (inner) {
|
||
diag.success = false;
|
||
diag.error = String(inner);
|
||
}
|
||
return diag;
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
// ==================== H17 消息发送 ====================
|
||
|
||
sendMessage: function (params) {
|
||
var toId = (params && params.to_id) || '';
|
||
var content = (params && params.content) || '';
|
||
var msgType = (params && params.msg_type) || 'text';
|
||
|
||
if (!toId || !content) {
|
||
return { success: false, error: '缺少 to_id 或 content' };
|
||
}
|
||
|
||
log('info', 'rpc', 'sendMessage', { to_id: toId, content_len: content.length, msg_type: msgType });
|
||
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _sendMessageInternal(toId, content, msgType);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
// ==================== H15 消息获取 ====================
|
||
|
||
getMessages: function (params) {
|
||
var limit = (params && params.limit) || 3000;
|
||
var offset = (params && params.offset) || 0;
|
||
var conversationId = (params && params.conversation_id) || '';
|
||
|
||
var dbRes = null;
|
||
try {
|
||
dbRes = Java.performNow(function () {
|
||
return _getMessagesFromDB(conversationId, limit, offset);
|
||
});
|
||
} catch (e) {
|
||
dbRes = { success: false, error: String(e), messages: [] };
|
||
}
|
||
// DB 读为空时 fallback 到 hook 实时缓冲(insert hook 已拦截新消息)
|
||
if (!dbRes || !dbRes.messages || dbRes.messages.length === 0) {
|
||
offset = parseInt(offset, 10) || 0;
|
||
if (offset < 0) offset = 0;
|
||
var filteredBuffer = _messageBuffer.filter(function (m) {
|
||
return !conversationId || m.from_id === conversationId;
|
||
});
|
||
var start = Math.max(filteredBuffer.length - offset - limit, 0);
|
||
var end = offset > 0 ? filteredBuffer.length - offset : filteredBuffer.length;
|
||
var buf = filteredBuffer.slice(start, end);
|
||
if (buf.length > 0) {
|
||
return {
|
||
success: true,
|
||
messages: buf.map(function (m) {
|
||
return { id: m.msg_svr_id, type: m.msg_type, from_id: m.from_id,
|
||
content: m.content, timestamp: m.timestamp, is_send: m.is_send };
|
||
}),
|
||
count: buf.length,
|
||
returned_count: buf.length,
|
||
requested_limit: limit,
|
||
offset: offset,
|
||
total_count: filteredBuffer.length,
|
||
has_more: filteredBuffer.length > offset + buf.length,
|
||
source: 'hook_buffer',
|
||
db_diag: _lastDbDiag,
|
||
};
|
||
}
|
||
if (dbRes) dbRes.db_diag = _lastDbDiag;
|
||
}
|
||
return dbRes;
|
||
},
|
||
|
||
getRecentMessages: function (params) {
|
||
var limit = (params && params.limit) || 3000;
|
||
var msgs = _messageBuffer.slice(-limit);
|
||
return { success: true, messages: msgs, count: msgs.length };
|
||
},
|
||
|
||
searchMessages: function (params) {
|
||
var keyword = (params && params.keyword) || '';
|
||
var limit = (params && params.limit) || 50;
|
||
if (!keyword) return { success: false, error: '缺少 keyword' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
var sql = "SELECT msgId, type, talker, content, createTime FROM message"
|
||
+ " WHERE content LIKE '%" + keyword.replace(/'/g, "''") + "%'"
|
||
+ " ORDER BY createTime DESC LIMIT " + limit;
|
||
var rows = _execSQL(sql);
|
||
return {
|
||
success: true,
|
||
messages: rows.map(function (r) {
|
||
return { id: r.msgId, type: r.type, from_id: r.talker, content: r.content, timestamp: r.createTime };
|
||
}),
|
||
count: rows.length,
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), messages: [] };
|
||
}
|
||
},
|
||
|
||
// ==================== H16 联系人 ====================
|
||
|
||
getContacts: function (params) {
|
||
var limit = (params && params.limit) || 10000;
|
||
var offset = (params && params.offset) || 0;
|
||
try {
|
||
return Java.performNow(function () { return _getContactsFromDB(limit, offset); });
|
||
} catch (e) {
|
||
return { success: false, error: String(e), contacts: [] };
|
||
}
|
||
},
|
||
|
||
getContactInfo: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
try {
|
||
return Java.performNow(function () { return _getContactInfoFromDB(wxid); });
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
searchContacts: function (params) {
|
||
var keyword = (params && params.keyword) || '';
|
||
var limit = (params && params.limit) || 50;
|
||
if (!keyword) return { success: false, error: '缺少 keyword' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
var kw = keyword.replace(/'/g, "''");
|
||
var sql = "SELECT username, nickname, conRemark, type FROM rcontact"
|
||
+ " WHERE (nickname LIKE '%" + kw + "%' OR conRemark LIKE '%" + kw + "%' OR username LIKE '%" + kw + "%')"
|
||
+ " AND type NOT IN (0, 33) AND verifyFlag=0"
|
||
+ " ORDER BY nickname LIMIT " + limit;
|
||
var rows = _execSQL(sql);
|
||
return {
|
||
success: true,
|
||
contacts: rows.map(function (r) {
|
||
return { wxid: r.username, nickname: r.nickname, remark: r.conRemark, type: r.type };
|
||
}),
|
||
count: rows.length,
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), contacts: [] };
|
||
}
|
||
},
|
||
|
||
// ==================== H18/H19 好友请求 ====================
|
||
|
||
getFriendRequests: function (params) {
|
||
var limit = (params && params.limit) || 50;
|
||
var cached = _friendRequestBuffer.slice(-limit);
|
||
if (cached.length > 0) return { success: true, requests: cached, count: cached.length, source: 'buffer' };
|
||
try {
|
||
return Java.performNow(function () { return _getFriendRequestsFromDB(limit); });
|
||
} catch (e) {
|
||
return { success: false, error: String(e), requests: [] };
|
||
}
|
||
},
|
||
|
||
addFriend: function (params) {
|
||
var userId = (params && params.user_id) || '';
|
||
var message = (params && params.message) || '你好,我想加你为好友';
|
||
var sourceType = (params && params.source_type) || 3; // 3=搜索
|
||
|
||
if (!userId) return { success: false, error: '缺少 user_id' };
|
||
log('info', 'rpc', 'addFriend', { user_id: userId, source_type: sourceType });
|
||
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _addFriendInternal(userId, message, sourceType);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
acceptFriend: function (params) {
|
||
var encryptUserName = (params && params.encrypt_username) || '';
|
||
var ticket = (params && params.ticket) || '';
|
||
|
||
if (!encryptUserName && !ticket) return { success: false, error: '缺少 encrypt_username 或 ticket' };
|
||
log('info', 'rpc', 'acceptFriend', { encrypt_username: encryptUserName });
|
||
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _acceptFriendInternal(encryptUserName, ticket);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
deleteFriend: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _deleteFriendInternal(wxid);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
setFriendRemark: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
var remark = (params && params.remark) || '';
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _setRemarkInternal(wxid, remark);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
// ==================== H22 群管理 ====================
|
||
|
||
getGroups: function (params) {
|
||
var limit = (params && params.limit) || 100;
|
||
var offset = (params && params.offset) || 0;
|
||
try {
|
||
return Java.performNow(function () { return _getGroupsFromDB(limit, offset); });
|
||
} catch (e) {
|
||
return { success: false, error: String(e), groups: [] };
|
||
}
|
||
},
|
||
|
||
getGroupMembers: function (params) {
|
||
var groupId = (params && params.group_id) || '';
|
||
if (!groupId) return { success: false, error: '缺少 group_id' };
|
||
try {
|
||
return Java.performNow(function () { return _getGroupMembersFromDB(groupId); });
|
||
} catch (e) {
|
||
return { success: false, error: String(e), members: [] };
|
||
}
|
||
},
|
||
|
||
getGroupInfo: function (params) {
|
||
var groupId = (params && params.group_id) || '';
|
||
if (!groupId) return { success: false, error: '缺少 group_id' };
|
||
try {
|
||
return Java.performNow(function () { return _getGroupInfoFromDB(groupId); });
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
sendGroupMessage: function (params) {
|
||
var groupId = (params && params.group_id) || '';
|
||
var content = (params && params.content) || '';
|
||
if (!groupId || !content) return { success: false, error: '缺少 group_id 或 content' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _sendMessageInternal(groupId, content, 'text');
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
createGroup: function (params) {
|
||
var memberIds = (params && params.member_ids) || [];
|
||
var topic = (params && params.topic) || '';
|
||
if (memberIds.length < 2) return { success: false, error: '建群至少需要2个成员' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _createGroupInternal(memberIds, topic);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
inviteToGroup: function (params) {
|
||
var groupId = (params && params.group_id) || '';
|
||
var memberIds = (params && params.member_ids) || [];
|
||
if (!groupId || memberIds.length === 0) return { success: false, error: '缺少 group_id 或 member_ids' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _inviteToGroupInternal(groupId, memberIds);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
removeFromGroup: function (params) {
|
||
var groupId = (params && params.group_id) || '';
|
||
var memberIds = (params && params.member_ids) || [];
|
||
if (!groupId || memberIds.length === 0) return { success: false, error: '缺少 group_id 或 member_ids' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _removeFromGroupInternal(groupId, memberIds);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
setGroupAnnouncement: function (params) {
|
||
var groupId = (params && params.group_id) || '';
|
||
var announcement = (params && params.announcement) || '';
|
||
if (!groupId) return { success: false, error: '缺少 group_id' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _setGroupAnnouncementInternal(groupId, announcement);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
setGroupName: function (params) {
|
||
var groupId = (params && params.group_id) || '';
|
||
var name = (params && params.name) || '';
|
||
if (!groupId || !name) return { success: false, error: '缺少 group_id 或 name' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _setGroupNameInternal(groupId, name);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
quitGroup: function (params) {
|
||
var groupId = (params && params.group_id) || '';
|
||
if (!groupId) return { success: false, error: '缺少 group_id' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _quitGroupInternal(groupId);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
// ==================== H20/H21 朋友圈 ====================
|
||
|
||
postMoments: function (params) {
|
||
var content = (params && params.content) || '';
|
||
var imageUrls = (params && params.image_urls) || [];
|
||
if (!content && imageUrls.length === 0) return { success: false, error: '朋友圈内容不能为空' };
|
||
var autoSend = !(params && params.auto_send === false);
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _postMomentsInternal(content, imageUrls, autoSend);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: '发朋友圈失败: ' + String(e) };
|
||
}
|
||
},
|
||
|
||
getMoments: function (params) {
|
||
var limit = (params && params.limit) || 20;
|
||
var wxid = (params && params.wxid) || '';
|
||
try {
|
||
return Java.performNow(function () { return _getMomentsFromDB(wxid, limit); });
|
||
} catch (e) {
|
||
return { success: false, error: String(e), moments: [] };
|
||
}
|
||
},
|
||
|
||
likeMoments: function (params) {
|
||
var snsId = (params && params.sns_id) || '';
|
||
if (!snsId) return { success: false, error: '缺少 sns_id' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _likeMomentsInternal(snsId);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
commentMoments: function (params) {
|
||
var snsId = (params && params.sns_id) || '';
|
||
var comment = (params && params.comment) || '';
|
||
if (!snsId || !comment) return { success: false, error: '缺少 sns_id 或 comment' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _commentMomentsInternal(snsId, comment);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
deleteMoments: function (params) {
|
||
var snsId = (params && params.sns_id) || '';
|
||
if (!snsId) return { success: false, error: '缺少 sns_id' };
|
||
return _intentAction('com.workphone.DELETE_MOMENTS', { sns_id: snsId }, 'moments_deleted', { sns_id: snsId });
|
||
},
|
||
|
||
// ==================== H23 账号管理 ====================
|
||
|
||
getProfile: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var sql = "SELECT value FROM userinfo WHERE id=4";
|
||
var rows = _execSQL(sql);
|
||
var nickname = rows.length > 0 ? rows[0].value : '';
|
||
var sql2 = "SELECT value FROM userinfo WHERE id=2";
|
||
var rows2 = _execSQL(sql2);
|
||
var wxid = rows2.length > 0 ? rows2[0].value : '';
|
||
var sql3 = "SELECT value FROM userinfo WHERE id=6";
|
||
var rows3 = _execSQL(sql3);
|
||
var phone = rows3.length > 0 ? rows3[0].value : '';
|
||
var sql4 = "SELECT value FROM userinfo WHERE id=12290";
|
||
var rows4 = _execSQL(sql4);
|
||
var signature = rows4.length > 0 ? rows4[0].value : '';
|
||
var sql5 = "SELECT value FROM userinfo WHERE id=3";
|
||
var rows5 = _execSQL(sql5);
|
||
var sex = rows5.length > 0 ? rows5[0].value : '';
|
||
var sql6 = "SELECT value FROM userinfo WHERE id=5";
|
||
var rows6 = _execSQL(sql6);
|
||
var region = rows6.length > 0 ? rows6[0].value : '';
|
||
return {
|
||
success: true,
|
||
profile: { nickname: nickname, wxid: wxid, phone: phone, signature: signature, sex: sex, region: region, wechat_version: _wechatVersion },
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
checkAccountStatus: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var ctx = getCtx();
|
||
var pm = ctx.getPackageManager();
|
||
var info = pm.getPackageInfo(jStr('com.tencent.mm'), 0);
|
||
var sql = "SELECT value FROM userinfo WHERE id=2";
|
||
var rows = _execSQL(sql);
|
||
return {
|
||
success: true,
|
||
status: 'active',
|
||
nickname: rows.length > 0 ? rows[0].value : '',
|
||
version: safeStr(info.versionName.value),
|
||
pid: Process.id,
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), status: 'unknown' };
|
||
}
|
||
},
|
||
|
||
setNickname: function (params) {
|
||
var nickname = (params && params.nickname) || '';
|
||
if (!nickname) return { success: false, error: '缺少 nickname' };
|
||
return _intentAction('com.workphone.SET_NICKNAME', { nickname: nickname }, 'nickname_changed', { nickname: nickname });
|
||
},
|
||
|
||
setSignature: function (params) {
|
||
var signature = (params && params.signature) || '';
|
||
return _intentAction('com.workphone.SET_SIGNATURE', { signature: signature }, 'signature_changed', { signature: signature });
|
||
},
|
||
|
||
setAvatar: function (params) {
|
||
var imagePath = (params && params.image_path) || '';
|
||
if (!imagePath) return { success: false, error: '缺少 image_path' };
|
||
return _intentAction('com.workphone.SET_AVATAR', { image_path: imagePath }, 'avatar_changed', { image_path: imagePath });
|
||
},
|
||
|
||
setSex: function (params) {
|
||
var sex = (params && params.sex) || '1';
|
||
return _intentAction('com.workphone.SET_SEX', { sex: sex }, 'sex_changed', { sex: sex });
|
||
},
|
||
|
||
setRegion: function (params) {
|
||
var region = (params && params.region) || '';
|
||
if (!region) return { success: false, error: '缺少 region' };
|
||
return _intentAction('com.workphone.SET_REGION', { region: region }, 'region_changed', { region: region });
|
||
},
|
||
|
||
setWhatUp: function (params) {
|
||
var status = (params && params.status) || '';
|
||
return _intentAction('com.workphone.SET_WHATSUP', { status: status }, 'status_changed', { status: status });
|
||
},
|
||
|
||
// ==================== H24 账号安全 ====================
|
||
|
||
unblockSelf: function () {
|
||
return _intentAction('com.workphone.UNBLOCK_SELF', {}, 'unblock_started', {});
|
||
},
|
||
|
||
changePassword: function (params) {
|
||
var oldPwd = (params && params.old_password) || '';
|
||
var newPwd = (params && params.new_password) || '';
|
||
if (!oldPwd || !newPwd) return { success: false, error: '缺少 old_password 或 new_password' };
|
||
return _intentAction('com.workphone.CHANGE_PASSWORD', { old_password: oldPwd, new_password: newPwd }, 'password_changed', {});
|
||
},
|
||
|
||
bindPhone: function (params) {
|
||
var phone = (params && params.phone) || '';
|
||
if (!phone) return { success: false, error: '缺少 phone' };
|
||
return _intentAction('com.workphone.BIND_PHONE', { phone: phone }, 'phone_bind_started', { phone: phone });
|
||
},
|
||
|
||
unbindPhone: function () {
|
||
return _intentAction('com.workphone.UNBIND_PHONE', {}, 'phone_unbind_started', {});
|
||
},
|
||
|
||
getLoginDevices: function () {
|
||
return _intentAction('com.workphone.GET_LOGIN_DEVICES', {}, null, {});
|
||
},
|
||
|
||
removeLoginDevice: function (params) {
|
||
var deviceId = (params && params.device_id) || '';
|
||
if (!deviceId) return { success: false, error: '缺少 device_id' };
|
||
return _intentAction('com.workphone.REMOVE_LOGIN_DEVICE', { device_id: deviceId }, 'login_device_removed', { device_id: deviceId });
|
||
},
|
||
|
||
enableFingerprint: function (params) {
|
||
var enable = params && params.enable !== undefined ? params.enable : true;
|
||
return _intentAction('com.workphone.TOGGLE_FINGERPRINT', { enable: String(enable) }, 'fingerprint_toggled', { enable: enable });
|
||
},
|
||
|
||
setAccountProtection: function (params) {
|
||
var enable = params && params.enable !== undefined ? params.enable : true;
|
||
return _intentAction('com.workphone.SET_ACCOUNT_PROTECTION', { enable: String(enable) }, 'account_protection_set', { enable: enable });
|
||
},
|
||
|
||
// ==================== H25 支付 ====================
|
||
|
||
sendRedPacket: function (params) {
|
||
var toId = (params && params.to_id) || '';
|
||
var amount = (params && params.amount) || '';
|
||
var message = (params && params.message) || '恭喜发财';
|
||
if (!toId || !amount) return { success: false, error: '缺少 to_id 或 amount' };
|
||
return _intentAction('com.workphone.SEND_RED_PACKET', { to_id: toId, amount: amount, message: message }, 'red_packet_sent', { to_id: toId, amount: amount });
|
||
},
|
||
|
||
receiveRedPacket: function (params) {
|
||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||
if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' };
|
||
return _intentAction('com.workphone.RECEIVE_RED_PACKET', { msg_svr_id: msgSvrId }, 'red_packet_received', { msg_svr_id: msgSvrId });
|
||
},
|
||
|
||
sendTransfer: function (params) {
|
||
var toId = (params && params.to_id) || '';
|
||
var amount = (params && params.amount) || '';
|
||
var desc = (params && params.description) || '';
|
||
if (!toId || !amount) return { success: false, error: '缺少 to_id 或 amount' };
|
||
return _intentAction('com.workphone.SEND_TRANSFER', { to_id: toId, amount: amount, description: desc }, 'transfer_sent', { to_id: toId, amount: amount });
|
||
},
|
||
|
||
receiveTransfer: function (params) {
|
||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||
if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' };
|
||
return _intentAction('com.workphone.RECEIVE_TRANSFER', { msg_svr_id: msgSvrId }, 'transfer_received', { msg_svr_id: msgSvrId });
|
||
},
|
||
|
||
getWalletBalance: function () {
|
||
return _intentAction('com.workphone.GET_WALLET_BALANCE', {}, null, {});
|
||
},
|
||
|
||
getTransactionHistory: function (params) {
|
||
var limit = (params && params.limit) || 20;
|
||
return _intentAction('com.workphone.GET_TRANSACTIONS', { limit: String(limit) }, null, {});
|
||
},
|
||
|
||
showPaymentCode: function () {
|
||
return _intentAction('com.workphone.SHOW_PAYMENT_CODE', {}, null, {});
|
||
},
|
||
|
||
getSafetyCenter: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var kws = ['账号暂时无法使用聊天功能', '功能受限', '账号异常', '暂时限制', '封禁', '被限制'];
|
||
var sql = 'SELECT content FROM message ORDER BY createTime DESC LIMIT 200';
|
||
var rows = _execSQL(sql);
|
||
var hits = [];
|
||
for (var i = 0; i < rows.length; i++) {
|
||
var c = String(rows[i].content || '');
|
||
for (var j = 0; j < kws.length; j++) {
|
||
if (c.indexOf(kws[j]) >= 0 && hits.indexOf(kws[j]) < 0) hits.push(kws[j]);
|
||
}
|
||
}
|
||
var bindMobile = '';
|
||
try {
|
||
var ui = _execSQL('SELECT value FROM userinfo WHERE type=4 LIMIT 1');
|
||
if (ui.length) bindMobile = String(ui[0].value || '');
|
||
} catch (_) {}
|
||
return {
|
||
success: true,
|
||
channel: 'frida_db',
|
||
restricted: hits.length > 0,
|
||
restrictions: hits,
|
||
bind_mobile_masked: bindMobile ? bindMobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
|
||
note: '账号安全摘要(DB+消息关键词,非 UI 导航)',
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
checkRestrictions: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var kws = ['账号暂时无法使用聊天功能', '功能受限', '账号异常', '暂时限制', '封禁', '被限制'];
|
||
var sql = 'SELECT content FROM message ORDER BY createTime DESC LIMIT 200';
|
||
var rows = _execSQL(sql);
|
||
var hits = [];
|
||
for (var i = 0; i < rows.length; i++) {
|
||
var c = String(rows[i].content || '');
|
||
for (var j = 0; j < kws.length; j++) {
|
||
if (c.indexOf(kws[j]) >= 0 && hits.indexOf(kws[j]) < 0) hits.push(kws[j]);
|
||
}
|
||
}
|
||
return { success: true, restricted: hits.length > 0, restrictions: hits };
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), restricted: false, restrictions: [] };
|
||
}
|
||
},
|
||
|
||
getTopStories: function () {
|
||
var limit = 5;
|
||
try {
|
||
return Java.performNow(function () {
|
||
var stories = [];
|
||
var probes = [
|
||
'SELECT title,url,pubTime FROM ReaderAppInfo ORDER BY rowid DESC LIMIT ' + limit,
|
||
'SELECT nickname,username FROM bizinfo WHERE type=1 LIMIT ' + limit,
|
||
'SELECT content,createTime FROM message WHERE type=49 ORDER BY createTime DESC LIMIT ' + limit,
|
||
];
|
||
for (var p = 0; p < probes.length && stories.length === 0; p++) {
|
||
try {
|
||
var rows = _execSQL(probes[p]);
|
||
for (var i = 0; i < rows.length; i++) {
|
||
stories.push(rows[i]);
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
return {
|
||
success: true,
|
||
channel: 'frida_db',
|
||
stories: stories,
|
||
count: stories.length,
|
||
note: stories.length ? '订阅号/ReaderApp DB 摘要' : '无 top_stories 可读行(版本/账号差异)',
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), stories: [] };
|
||
}
|
||
},
|
||
|
||
getWechatSteps: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var steps = 0;
|
||
var detail = [];
|
||
var sportPath = null;
|
||
try {
|
||
var File = Java.use('java.io.File');
|
||
var dir = File.$new('/data/data/com.tencent.mm/MicroMsg/');
|
||
if (dir.exists()) {
|
||
var subs = dir.listFiles();
|
||
if (subs) {
|
||
for (var i = 0; i < subs.length; i++) {
|
||
var cand = subs[i].getAbsolutePath() + '/sportrecord.db';
|
||
if (File.$new(cand).exists()) { sportPath = cand; break; }
|
||
}
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
if (sportPath) {
|
||
var rows = _execSQL('SELECT * FROM SportRecordInfo ORDER BY rowid DESC LIMIT 1', sportPath);
|
||
if (rows.length) {
|
||
var r0 = rows[0];
|
||
steps = parseInt(r0.step || r0.steps || r0.Step || '0', 10) || 0;
|
||
detail = rows;
|
||
}
|
||
}
|
||
if (!steps) {
|
||
try {
|
||
var alt = _execSQL("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%sport%'");
|
||
if (alt.length) detail = alt;
|
||
} catch (_) {}
|
||
}
|
||
return {
|
||
success: true,
|
||
channel: 'frida_db',
|
||
steps: steps,
|
||
records: detail,
|
||
sport_db: sportPath || '',
|
||
note: steps ? 'sportrecord.db 最近步数' : '步数为 0 或未开启微信运动(诚实返回)',
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), steps: 0 };
|
||
}
|
||
},
|
||
|
||
getStickerList: function (params) {
|
||
// 真实现:读 EnMicroMsg.db 的 EmojiInfo 表(自定义表情);动态列探测兼容 8.0.x;
|
||
// 表不存在/无自定义表情时诚实返回空,不假成功(真机铁律 #3/#14)。
|
||
var limit = (params && params.limit) || 100;
|
||
try {
|
||
return Java.performNow(function () {
|
||
var cols;
|
||
try { cols = _execSQL("PRAGMA table_info(EmojiInfo)"); } catch (e0) { cols = []; }
|
||
if (!cols || cols.length === 0) {
|
||
return { success: true, stickers: [], count: 0, channel: 'frida_db',
|
||
note: 'EmojiInfo 表不存在或无自定义表情(版本/账号差异)' };
|
||
}
|
||
var names = cols.map(function (c) { return c.name; });
|
||
var want = ['md5', 'catalog', 'name', 'type', 'size', 'cdnUrl', 'groupId', 'reserved4'];
|
||
var sel = want.filter(function (w) { return names.indexOf(w) >= 0; });
|
||
if (sel.length === 0) sel = names.slice(0, 6);
|
||
var rows = _execSQL("SELECT " + sel.join(',') + " FROM EmojiInfo LIMIT " + limit);
|
||
return {
|
||
success: true,
|
||
channel: 'frida_db',
|
||
columns: sel,
|
||
stickers: rows.map(function (r) {
|
||
return {
|
||
md5: r.md5, name: r.name, catalog: r.catalog,
|
||
type: r.type, size: r.size,
|
||
cdn_url: r.cdnUrl || r.reserved4 || '', group_id: r.groupId
|
||
};
|
||
}),
|
||
count: rows.length,
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), stickers: [] };
|
||
}
|
||
},
|
||
|
||
likeWechatSteps: function (params) {
|
||
var userId = (params && params.user_id) || '';
|
||
return _intentAction('com.workphone.LIKE_WECHAT_STEPS', { user_id: userId }, 'steps_liked', { user_id: userId });
|
||
},
|
||
|
||
clearCache: function () {
|
||
return _intentAction('com.workphone.CLEAR_CACHE', {}, 'cache_cleared', {});
|
||
},
|
||
|
||
checkForUpdate: function () {
|
||
return _intentAction('com.workphone.CHECK_UPDATE', {}, 'update_checked', {});
|
||
},
|
||
|
||
// ==================== H26 二维码 ====================
|
||
|
||
scanQrCode: function (params) {
|
||
var imagePath = (params && params.image_path) || '';
|
||
if (!imagePath) {
|
||
return {
|
||
success: false,
|
||
error: 'missing_image_path',
|
||
hint: 'extract_qr_from_image 需 image_path;或走 u2 相册导航(hook_only=0)',
|
||
channel: 'frida_rpc',
|
||
};
|
||
}
|
||
return _intentAction('com.workphone.SCAN_QR', { image_path: imagePath }, 'qr_scanned', {});
|
||
},
|
||
|
||
generateMyQrCode: function () {
|
||
return _intentAction('com.workphone.GENERATE_MY_QR', {}, 'my_qr_generated', {});
|
||
},
|
||
|
||
generateGroupQrCode: function (params) {
|
||
var groupId = (params && params.group_id) || '';
|
||
if (!groupId) return { success: false, error: '缺少 group_id' };
|
||
return _intentAction('com.workphone.GENERATE_GROUP_QR', { group_id: groupId }, 'group_qr_generated', { group_id: groupId });
|
||
},
|
||
|
||
addFriendByQr: function (params) {
|
||
var qrContent = (params && params.qr_content) || '';
|
||
if (!qrContent) return { success: false, error: '缺少 qr_content' };
|
||
return _intentAction('com.workphone.ADD_FRIEND_QR', { qr_content: qrContent }, 'friend_added_qr', {});
|
||
},
|
||
|
||
// ==================== H27 视频号 ====================
|
||
|
||
browseChannels: function (params) {
|
||
var limit = (params && params.limit) || 10;
|
||
return _intentAction('com.workphone.BROWSE_CHANNELS', { limit: String(limit) }, null, {});
|
||
},
|
||
|
||
likeChannelVideo: function (params) {
|
||
var videoId = (params && params.video_id) || '';
|
||
if (!videoId && params && params.index !== undefined) {
|
||
videoId = 'index_' + params.index;
|
||
}
|
||
if (!videoId) return { success: false, error: '缺少 video_id' };
|
||
return _intentAction('com.workphone.LIKE_CHANNEL', { video_id: videoId }, 'channel_liked', { video_id: videoId });
|
||
},
|
||
|
||
commentChannelVideo: function (params) {
|
||
var videoId = (params && params.video_id) || '';
|
||
var comment = (params && params.comment) || '';
|
||
if (!videoId || !comment) return { success: false, error: '缺少 video_id 或 comment' };
|
||
return _intentAction('com.workphone.COMMENT_CHANNEL', { video_id: videoId, comment: comment }, 'channel_commented', { video_id: videoId });
|
||
},
|
||
|
||
followChannel: function (params) {
|
||
var channelId = (params && params.channel_id) || '';
|
||
if (!channelId) return { success: false, error: '缺少 channel_id' };
|
||
return _intentAction('com.workphone.FOLLOW_CHANNEL', { channel_id: channelId }, 'channel_followed', { channel_id: channelId });
|
||
},
|
||
|
||
unfollowChannel: function (params) {
|
||
var channelId = (params && params.channel_id) || '';
|
||
if (!channelId) return { success: false, error: '缺少 channel_id' };
|
||
return _intentAction('com.workphone.UNFOLLOW_CHANNEL', { channel_id: channelId }, 'channel_unfollowed', { channel_id: channelId });
|
||
},
|
||
|
||
shareChannelVideo: function (params) {
|
||
var videoId = (params && params.video_id) || '';
|
||
var toId = (params && params.to_id) || '';
|
||
if (!videoId || !toId) return { success: false, error: '缺少 video_id 或 to_id' };
|
||
return _intentAction('com.workphone.SHARE_CHANNEL', { video_id: videoId, to_id: toId }, 'channel_shared', { video_id: videoId, to_id: toId });
|
||
},
|
||
|
||
// ==================== H28 标签管理 ====================
|
||
|
||
getLabels: function (params) {
|
||
var limit = (params && params.limit) || 5000;
|
||
var offset = (params && params.offset) || 0;
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _getLabelsFromDB(limit, offset);
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), labels: [] };
|
||
}
|
||
},
|
||
|
||
createLabel: function (params) {
|
||
var name = (params && params.name) || '';
|
||
if (!name) return { success: false, error: '缺少 name' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
var nextId = _nextLabelId();
|
||
var esc = _escapeSql(name);
|
||
var idStr = String(nextId);
|
||
var wr = _execSQLWrite(
|
||
"INSERT INTO contactlabel (labelID, labelName) VALUES ('" + idStr + "', '" + esc + "')"
|
||
);
|
||
if (!wr.success) {
|
||
wr = _execSQLWrite(
|
||
"INSERT INTO contactlabelinfo (labelId, labelName) VALUES ('" + idStr + "', '" + esc + "')"
|
||
);
|
||
}
|
||
if (!wr.success) return { success: false, error: wr.error, method: 'wcdb_insert' };
|
||
_tryNotifyLabelChanged();
|
||
return { success: true, label_id: idStr, name: name, method: 'wcdb_insert' };
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
deleteLabel: function (params) {
|
||
var labelId = (params && (params.label_id || params.labelId)) || '';
|
||
if (!labelId) return { success: false, error: '缺少 label_id' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
var escId = _escapeSql(labelId);
|
||
var wr = _execSQLWrite("DELETE FROM contactlabel WHERE labelID='" + escId + "'");
|
||
if (!wr.success) {
|
||
wr = _execSQLWrite("DELETE FROM contactlabelinfo WHERE labelId='" + escId + "'");
|
||
}
|
||
if (!wr.success) return { success: false, error: wr.error, method: 'wcdb_delete' };
|
||
_execSQLWrite(
|
||
"UPDATE rcontact SET contactLabelIds=REPLACE(REPLACE(REPLACE(contactLabelIds,'" + escId + ",',''),'," + escId + "',''),'" + escId + "','') WHERE contactLabelIds LIKE '%" + escId + "%'"
|
||
);
|
||
_execSQLWrite(
|
||
"UPDATE rcontact SET labelidlist=REPLACE(REPLACE(REPLACE(labelidlist,'" + escId + ",',''),'," + escId + "',''),'" + escId + "','') WHERE labelidlist LIKE '%" + escId + "%'"
|
||
);
|
||
_tryNotifyLabelChanged();
|
||
return { success: true, label_id: String(labelId), method: 'wcdb_delete' };
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
setContactLabel: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
var labelIds = _normalizeLabelIds((params && params.label_ids) || (params && params.labelIds) || []);
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
var escWxid = _escapeSql(wxid);
|
||
var joined = labelIds.join(',');
|
||
var escJoined = _escapeSql(joined);
|
||
var wr = _execSQLWrite(
|
||
"UPDATE rcontact SET contactLabelIds='" + escJoined + "' WHERE username='" + escWxid + "'"
|
||
);
|
||
if (!wr.success) {
|
||
wr = _execSQLWrite(
|
||
"UPDATE rcontact SET labelidlist='" + escJoined + "' WHERE username='" + escWxid + "'"
|
||
);
|
||
}
|
||
if (!wr.success) return { success: false, error: wr.error, method: 'wcdb_update' };
|
||
_tryNotifyLabelChanged();
|
||
return { success: true, wxid: wxid, label_ids: labelIds, method: 'wcdb_update' };
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
getContactsByLabel: function (params) {
|
||
var labelId = (params && params.label_id) || '';
|
||
if (!labelId) return { success: false, error: '缺少 label_id' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
var sql = "SELECT username, nickname, conRemark FROM rcontact WHERE contactLabelIds LIKE '%" + labelId + "%' AND type NOT IN (0,33) AND verifyFlag=0 ORDER BY nickname LIMIT 200";
|
||
var rows = _execSQL(sql);
|
||
return { success: true, contacts: rows.map(function (r) { return { wxid: r.username, nickname: r.nickname, remark: r.conRemark }; }), count: rows.length };
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), contacts: [] };
|
||
}
|
||
},
|
||
|
||
// ==================== H29 收藏管理 ====================
|
||
|
||
getFavorites: function (params) {
|
||
var limit = (params && params.limit) || 50;
|
||
try {
|
||
return Java.performNow(function () {
|
||
var names = ['Favorite.db', 'favorite.db', 'EnFavorite.db'];
|
||
var favPath = null;
|
||
for (var i = 0; i < names.length; i++) {
|
||
favPath = _findDbPath(names[i]);
|
||
if (favPath) break;
|
||
}
|
||
if (!favPath) {
|
||
var sql2 = "SELECT localId, type, sourceUserName, updateTime FROM FavItemInfo ORDER BY updateTime DESC LIMIT " + limit;
|
||
var rows2 = _execSQL(sql2);
|
||
if (rows2.length > 0) return { success: true, favorites: rows2, count: rows2.length, source: 'main_db' };
|
||
return { success: true, favorites: [], count: 0, note: 'Favorite 表不在独立数据库中,主库也未找到' };
|
||
}
|
||
var sql = "SELECT localId, type, sourceUserName, updateTime FROM FavItemInfo ORDER BY updateTime DESC LIMIT " + limit;
|
||
var rows = _execSQL(sql, favPath);
|
||
return { success: true, favorites: rows, count: rows.length };
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), favorites: [] };
|
||
}
|
||
},
|
||
|
||
addFavorite: function (params) {
|
||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||
var type = (params && params.type) || 'message';
|
||
var content = (params && params.content) || (params && params.content_desc) || '';
|
||
if (!msgSvrId && content) {
|
||
return _intentAction('com.workphone.ADD_FAVORITE_TEXT', { content: content, type: type }, 'favorite_added', { content: content });
|
||
}
|
||
if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id 或 content' };
|
||
return _intentAction('com.workphone.ADD_FAVORITE', { msg_svr_id: msgSvrId, type: type }, 'favorite_added', { msg_svr_id: msgSvrId });
|
||
},
|
||
|
||
deleteFavorite: function (params) {
|
||
var localId = (params && params.local_id) || '';
|
||
if (!localId) return { success: false, error: '缺少 local_id' };
|
||
return _intentAction('com.workphone.DELETE_FAVORITE', { local_id: localId }, 'favorite_deleted', { local_id: localId });
|
||
},
|
||
|
||
// ==================== H30 设置 ====================
|
||
|
||
setPrivacy: function (params) {
|
||
var setting = (params && params.setting) || '';
|
||
var value = params && params.value !== undefined ? params.value : true;
|
||
if (!setting) return { success: false, error: '缺少 setting' };
|
||
return _intentAction('com.workphone.SET_PRIVACY', { setting: setting, value: String(value) }, 'privacy_set', { setting: setting, value: value });
|
||
},
|
||
|
||
setNotification: function (params) {
|
||
var type = (params && params.type) || 'all';
|
||
var enable = params && params.enable !== undefined ? params.enable : true;
|
||
return _intentAction('com.workphone.SET_NOTIFICATION', { type: type, enable: String(enable) }, 'notification_set', { type: type, enable: enable });
|
||
},
|
||
|
||
clearChatHistory: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
return _intentAction('com.workphone.CLEAR_CHAT', { wxid: wxid }, 'chat_cleared', { wxid: wxid });
|
||
},
|
||
|
||
setChatBackground: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
var imagePath = (params && params.image_path) || '';
|
||
return _intentAction('com.workphone.SET_CHAT_BG', { wxid: wxid, image_path: imagePath }, 'chat_bg_set', { wxid: wxid });
|
||
},
|
||
|
||
setDoNotDisturb: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
var enable = params && params.enable !== undefined ? params.enable : true;
|
||
if (!wxid) {
|
||
return _intentAction('com.workphone.SET_GLOBAL_DND', { enable: String(enable) }, 'dnd_set', { enable: enable });
|
||
}
|
||
return _intentAction('com.workphone.SET_DND', { wxid: wxid, enable: String(enable) }, 'dnd_set', { wxid: wxid, enable: enable });
|
||
},
|
||
|
||
pinChat: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
var pin = params && params.pin !== undefined ? params.pin : true;
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
return _intentAction('com.workphone.PIN_CHAT', { wxid: wxid, pin: String(pin) }, 'chat_pinned', { wxid: wxid, pin: pin });
|
||
},
|
||
|
||
// ==================== H31 搜索 ====================
|
||
|
||
globalSearch: function (params) {
|
||
var keyword = (params && params.keyword) || '';
|
||
var limit = (params && params.limit) || 30;
|
||
if (!keyword) return { success: false, error: '缺少 keyword' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
var kw = keyword.replace(/'/g, "''");
|
||
var contactSql = "SELECT username, nickname, conRemark FROM rcontact WHERE (nickname LIKE '%" + kw + "%' OR conRemark LIKE '%" + kw + "%') AND type NOT IN (0,33) AND verifyFlag=0 LIMIT " + limit;
|
||
var contacts = _execSQL(contactSql);
|
||
var msgSql = "SELECT msgId, talker, content, createTime FROM message WHERE content LIKE '%" + kw + "%' ORDER BY createTime DESC LIMIT " + limit;
|
||
var messages = _execSQL(msgSql);
|
||
return {
|
||
success: true,
|
||
contacts: contacts.map(function (r) { return { wxid: r.username, nickname: r.nickname, remark: r.conRemark }; }),
|
||
messages: messages.map(function (r) { return { id: r.msgId, from_id: r.talker, content: r.content, timestamp: r.createTime }; }),
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
// ==================== H32 小程序 ====================
|
||
|
||
openMiniProgram: function (params) {
|
||
var appId = (params && params.app_id) || (params && params.name) || '';
|
||
var path = (params && params.path) || '';
|
||
if (!appId) return { success: false, error: '缺少 app_id' };
|
||
return _intentAction('com.workphone.OPEN_MINI_PROGRAM', { app_id: appId, path: path }, 'mini_program_opened', { app_id: appId });
|
||
},
|
||
|
||
getRecentMiniPrograms: function () {
|
||
return _intentAction('com.workphone.GET_RECENT_MINI_PROGRAMS', {}, null, {});
|
||
},
|
||
|
||
shareMiniProgram: function (params) {
|
||
var appId = (params && params.app_id) || '';
|
||
var toId = (params && params.to_id) || '';
|
||
var title = (params && params.title) || '';
|
||
if (!appId || !toId) return { success: false, error: '缺少 app_id 或 to_id' };
|
||
return _intentAction('com.workphone.SHARE_MINI_PROGRAM', { app_id: appId, to_id: toId, title: title }, 'mini_program_shared', { app_id: appId, to_id: toId });
|
||
},
|
||
|
||
// ==================== H33 文件传输 ====================
|
||
|
||
sendImage: function (params) {
|
||
var toId = (params && params.to_id) || '';
|
||
var imagePath = (params && params.image_path) || '';
|
||
if (!toId || !imagePath) return { success: false, error: '缺少 to_id 或 image_path' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _shareMediaToWechat(imagePath, 'image/*', toId, 'image');
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
sendVideo: function (params) {
|
||
var toId = (params && params.to_id) || '';
|
||
var videoPath = (params && params.video_path) || '';
|
||
if (!toId || !videoPath) return { success: false, error: '缺少 to_id 或 video_path' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _shareMediaToWechat(videoPath, 'video/*', toId, 'video');
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
sendFile: function (params) {
|
||
var toId = (params && params.to_id) || '';
|
||
var filePath = (params && params.file_path) || '';
|
||
if (!toId || !filePath) return { success: false, error: '缺少 to_id 或 file_path' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
return _shareMediaToWechat(filePath, '*/*', toId, 'file');
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
sendVoice: function (params) {
|
||
var toId = (params && params.to_id) || (params && params.user_id) || '';
|
||
var voicePath = (params && params.voice_path) || '';
|
||
var duration = (params && params.duration) || 0;
|
||
if (!toId) return { success: false, error: '缺少 to_id' };
|
||
if (!voicePath && duration) {
|
||
return _intentAction('com.workphone.SEND_VOICE_RECORD', { to_id: toId, duration: String(duration) }, 'voice_sent', { to_id: toId, duration: duration });
|
||
}
|
||
if (!voicePath) return { success: false, error: '缺少 voice_path' };
|
||
return _intentAction('com.workphone.SEND_VOICE', { to_id: toId, voice_path: voicePath, duration: String(duration) }, 'voice_sent', { to_id: toId });
|
||
},
|
||
|
||
sendLocation: function (params) {
|
||
var toId = (params && params.to_id) || '';
|
||
var lat = (params && params.latitude) || '';
|
||
var lng = (params && params.longitude) || '';
|
||
var label = (params && params.label) || '';
|
||
if (!toId || !lat || !lng) return { success: false, error: '缺少 to_id/latitude/longitude' };
|
||
return _intentAction('com.workphone.SEND_LOCATION', { to_id: toId, latitude: lat, longitude: lng, label: label }, 'location_sent', { to_id: toId });
|
||
},
|
||
|
||
sendCard: function (params) {
|
||
var toId = (params && params.to_id) || '';
|
||
var cardWxid = (params && params.card_wxid) || '';
|
||
if (!toId || !cardWxid) return { success: false, error: '缺少 to_id 或 card_wxid' };
|
||
return _intentAction('com.workphone.SEND_CARD', { to_id: toId, card_wxid: cardWxid }, 'card_sent', { to_id: toId, card_wxid: cardWxid });
|
||
},
|
||
|
||
sendLink: function (params) {
|
||
var toId = (params && params.to_id) || '';
|
||
var url = (params && params.url) || '';
|
||
var title = (params && params.title) || '';
|
||
var desc = (params && params.description) || '';
|
||
if (!toId || !url) return { success: false, error: '缺少 to_id 或 url' };
|
||
var _linkText = (title ? title + '\n' : '') + (desc ? desc + '\n' : '') + url;
|
||
return _shareTextToWechat(_linkText, toId);
|
||
},
|
||
|
||
// ==================== H34 消息转发 ====================
|
||
|
||
forwardMessage: function (params) {
|
||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||
var toId = (params && params.to_id) || '';
|
||
if (!msgSvrId || !toId) return { success: false, error: '缺少 msg_svr_id 或 to_id' };
|
||
// 真 Frida 转发:文本类(type=1)从 DB 取原文 → 复用已验证 _sendMessageInternal(SendMsgEvent + DB 回查)。
|
||
// 注意:_intentAction 内部自带 Java.performNow,禁止嵌套调用(会死锁挂起);故降级在 performNow 外执行。
|
||
var res = Java.performNow(function () {
|
||
try {
|
||
// 调用方可能传服务端 msgSvrId,也可能传 get_messages 返回的本地 msgId,两者都匹配。
|
||
var _id = String(msgSvrId).replace(/'/g, "''");
|
||
var sql = "SELECT type, content FROM message WHERE msgSvrId = '" + _id
|
||
+ "' OR msgId = '" + _id + "' LIMIT 1";
|
||
var rows = _execSQL(sql);
|
||
// 文本类:WeChat type=1 为标准文本;本机 filehelper/SendMsgEvent 落库可能为 0。
|
||
// 仅当 type ∈ {0,1} 且 content 为非空纯文本(不以媒体 XML 前缀开头)才文本重发,避免误转媒体。
|
||
var _t = rows && rows.length > 0 ? String(rows[0].type) : '';
|
||
var _c = rows && rows.length > 0 ? (rows[0].content || '') : '';
|
||
var _isText = (_t === '1' || _t === '0') && _c && _c.charAt(0) !== '<';
|
||
if (_isText) {
|
||
var r = _sendMessageInternal(toId, _c, 'text');
|
||
if (r && r.success) {
|
||
r.forwarded_from = msgSvrId;
|
||
r.forward_kind = 'text_resend';
|
||
emitEvent('message_forwarded', { msg_svr_id: msgSvrId, to_id: toId, kind: 'text_resend' });
|
||
return r;
|
||
}
|
||
}
|
||
return { _fallback: true };
|
||
} catch (e) {
|
||
return { _fallback: true, _err: String(e) };
|
||
}
|
||
});
|
||
if (res && res._fallback) {
|
||
// 非文本/DB 未命中/真发送未验证 → companion intent 降级(在 performNow 外,避免嵌套死锁)
|
||
return _intentAction('com.workphone.FORWARD_MESSAGE', { msg_svr_id: msgSvrId, to_id: toId }, 'message_forwarded', { msg_svr_id: msgSvrId, to_id: toId, kind: 'non_text_or_db_miss' });
|
||
}
|
||
return res;
|
||
},
|
||
|
||
forwardMultiple: function (params) {
|
||
var msgSvrIds = (params && params.msg_svr_ids) || [];
|
||
var toId = (params && params.to_id) || '';
|
||
if (msgSvrIds.length === 0 || !toId) return { success: false, error: '缺少 msg_svr_ids 或 to_id' };
|
||
// 真 Frida 批量转发:逐条复用文本转发真实路径(DB 取原文 → _sendMessageInternal)。
|
||
// 文本条真发送成功才计 forwarded;非文本/未验证条诚实计入 fallback,不伪造整体成功。
|
||
// 注意:_intentAction 自带 Java.performNow,降级须在 performNow 外执行(避免嵌套死锁挂起)。
|
||
var agg = Java.performNow(function () {
|
||
var forwarded = 0, fallback = 0, details = [];
|
||
for (var i = 0; i < msgSvrIds.length; i++) {
|
||
var id = String(msgSvrIds[i]);
|
||
try {
|
||
var sql = "SELECT type, content FROM message WHERE msgSvrId = '"
|
||
+ id.replace(/'/g, "''") + "' LIMIT 1";
|
||
var rows = _execSQL(sql);
|
||
if (rows && rows.length > 0 && (rows[0].type === 1 || String(rows[0].type) === '1') && rows[0].content) {
|
||
var res = _sendMessageInternal(toId, rows[0].content, 'text');
|
||
if (res && res.success) { forwarded++; details.push({ msg_svr_id: id, kind: 'text_resend', success: true }); continue; }
|
||
}
|
||
} catch (e) { /* 落入 fallback */ }
|
||
fallback++;
|
||
details.push({ msg_svr_id: id, kind: 'non_text_or_db_miss', success: false });
|
||
}
|
||
return { forwarded: forwarded, fallback: fallback, details: details };
|
||
});
|
||
if (agg.forwarded > 0) {
|
||
return { success: true, forwarded: agg.forwarded, fallback: agg.fallback, total: msgSvrIds.length, details: agg.details };
|
||
}
|
||
// 无一条真发送成功 → 整体降级 intent(performNow 外,避免嵌套死锁)
|
||
return _intentAction('com.workphone.FORWARD_MULTIPLE', { msg_svr_ids: msgSvrIds.join(','), to_id: toId }, 'messages_forwarded', { count: msgSvrIds.length, to_id: toId, forwarded: 0, fallback: agg.fallback });
|
||
},
|
||
|
||
revokeMessage: function (params) {
|
||
var msgSvrId = (params && params.msg_svr_id) || '';
|
||
if (!msgSvrId) return { success: false, error: '缺少 msg_svr_id' };
|
||
// 撤回资格预检(DB · 复用已验证 _execSQL):消息须存在、为本人发送(isSend=1)、且在 2 分钟窗口内。
|
||
// 不满足直接诚实返回原因,避免对不可撤回消息发 intent 假成功。真撤回 NetScene 仍待真机实现。
|
||
// 注意:_intentAction 自带 Java.performNow,资格通过后的 intent 须在 performNow 外执行(避免嵌套死锁)。
|
||
var pre = Java.performNow(function () {
|
||
try {
|
||
var _rid = String(msgSvrId).replace(/'/g, "''");
|
||
var sql = "SELECT isSend, createTime FROM message WHERE msgSvrId = '" + _rid
|
||
+ "' OR msgId = '" + _rid + "' LIMIT 1";
|
||
var rows = _execSQL(sql);
|
||
if (!rows || rows.length === 0) {
|
||
return { ok: false, result: { success: false, error: '消息不存在或已删除', error_code: 'msg_not_found', msg_svr_id: msgSvrId } };
|
||
}
|
||
var row = rows[0];
|
||
if (String(row.isSend) !== '1') {
|
||
return { ok: false, result: { success: false, error: '仅可撤回本人发送的消息', error_code: 'not_own_message', msg_svr_id: msgSvrId } };
|
||
}
|
||
var createMs = parseInt(row.createTime, 10) || 0;
|
||
var ageMs = Date.now() - createMs;
|
||
if (createMs > 0 && ageMs > 120000) {
|
||
return { ok: false, result: { success: false, error: '超过 2 分钟撤回窗口', error_code: 'revoke_window_expired',
|
||
age_seconds: Math.round(ageMs / 1000), msg_svr_id: msgSvrId } };
|
||
}
|
||
return { ok: true, row: row };
|
||
} catch (e) {
|
||
return { ok: false, result: { success: false, error: String(e), msg_svr_id: msgSvrId } };
|
||
}
|
||
});
|
||
if (!pre.ok) return pre.result;
|
||
// 真 Frida 撤回:RevokeMsgEvent + IEvent.e(8.0.x autogen,参照 SendMsgEvent)
|
||
var fridaRes = Java.performNow(function () {
|
||
return _revokeMessageByRevokeMsgEvent(msgSvrId, pre.row);
|
||
});
|
||
if (fridaRes && fridaRes.success && fridaRes.verified) {
|
||
emitEvent('message_revoked', { msg_svr_id: msgSvrId, method: 'revoke_msg_event' });
|
||
return fridaRes;
|
||
}
|
||
// 真 RPC 未 DB 确认 → 诚实返回,不伪造 intent 成功
|
||
return fridaRes || { success: false, error: 'revoke_frida_failed', msg_svr_id: msgSvrId, eligible: true };
|
||
},
|
||
|
||
// ==================== H35 注册/登录 ====================
|
||
|
||
registerAccount: function (params) {
|
||
var phone = (params && params.phone) || '';
|
||
var nickname = (params && params.nickname) || '';
|
||
var password = (params && params.password) || '';
|
||
if (!phone) return { success: false, error: '缺少 phone' };
|
||
return _intentAction('com.workphone.REGISTER', { phone: phone, nickname: nickname, password: password }, 'register_started', { phone: phone });
|
||
},
|
||
|
||
loginByPassword: function (params) {
|
||
var phone = (params && params.phone) || '';
|
||
var password = (params && params.password) || '';
|
||
if (!phone || !password) return { success: false, error: '缺少 phone 或 password' };
|
||
return _intentAction('com.workphone.LOGIN_PASSWORD', { phone: phone, password: password }, 'login_started', { phone: phone });
|
||
},
|
||
|
||
loginBySms: function (params) {
|
||
var phone = (params && params.phone) || '';
|
||
if (!phone) return { success: false, error: '缺少 phone' };
|
||
return _intentAction('com.workphone.LOGIN_SMS', { phone: phone }, 'sms_login_started', { phone: phone });
|
||
},
|
||
|
||
logout: function () {
|
||
return _intentAction('com.workphone.LOGOUT', {}, 'logout_started', {});
|
||
},
|
||
|
||
switchAccount: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
return _intentAction('com.workphone.SWITCH_ACCOUNT', { wxid: wxid }, 'account_switched', { wxid: wxid || 'picker' });
|
||
},
|
||
|
||
voiceCall: function (params) {
|
||
var uid = (params && (params.user_id || params.to_id)) || '';
|
||
return _intentAction('com.workphone.VOICE_CALL', { user_id: uid }, 'voice_call', { user_id: uid });
|
||
},
|
||
|
||
videoCall: function (params) {
|
||
var uid = (params && (params.user_id || params.to_id)) || '';
|
||
return _intentAction('com.workphone.VIDEO_CALL', { user_id: uid }, 'video_call', { user_id: uid });
|
||
},
|
||
|
||
shareRealTimeLocation: function (params) {
|
||
var uid = (params && (params.user_id || params.to_id)) || '';
|
||
return _intentAction('com.workphone.SHARE_RT_LOCATION', { user_id: uid }, 'rt_location', { user_id: uid });
|
||
},
|
||
|
||
downloadFile: function (params) {
|
||
var uid = (params && (params.user_id || params.to_id)) || '';
|
||
return _intentAction('com.workphone.DOWNLOAD_FILE', { user_id: uid }, 'file_downloaded', { user_id: uid });
|
||
},
|
||
|
||
massSend: function (params) {
|
||
var ids = (params && params.user_ids) || [];
|
||
var content = (params && params.content) || '';
|
||
if (!content || !ids.length) return { success: false, error: '缺少 content 或 user_ids' };
|
||
return _intentAction('com.workphone.MASS_SEND', { user_ids: ids.join(','), content: content }, 'mass_send', { count: ids.length });
|
||
},
|
||
|
||
replyComment: function (params) {
|
||
return _intentAction('com.workphone.REPLY_COMMENT', {
|
||
video_id: (params && params.video_id) || '',
|
||
comment_id: (params && params.comment_id) || '',
|
||
content: (params && params.content) || '',
|
||
}, 'reply_comment', params || {});
|
||
},
|
||
|
||
receivePayment: function (params) {
|
||
return _intentAction('com.workphone.RECEIVE_PAYMENT', {
|
||
amount: (params && params.amount) || '',
|
||
desc: (params && params.desc) || '',
|
||
}, 'payment_received', params || {});
|
||
},
|
||
|
||
autoRegister: function (params) {
|
||
var phone = (params && params.phone) || '';
|
||
var nickname = (params && params.nickname) || '卡若AI';
|
||
var password = (params && params.password) || '';
|
||
return _intentAction('com.workphone.AUTO_REGISTER', {
|
||
phone: phone, nickname: nickname, password: password
|
||
}, 'auto_register_started', { phone: phone, nickname: nickname });
|
||
},
|
||
|
||
checkLoginState: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var ctx = getCtx();
|
||
var am = ctx.getSystemService(jStr('activity'));
|
||
var sql = "SELECT value FROM userinfo WHERE id=2";
|
||
var rows = _execSQL(sql);
|
||
var hasUser = rows.length > 0 && rows[0].value && rows[0].value.length > 0;
|
||
return {
|
||
success: true,
|
||
logged_in: hasUser,
|
||
nickname: hasUser ? rows[0].value : '',
|
||
state: hasUser ? 'logged_in' : 'not_logged_in',
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, logged_in: false, state: 'unknown', error: String(e) };
|
||
}
|
||
},
|
||
|
||
getSimPhone: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var ctx = getCtx();
|
||
var TelephonyManager = Java.use('android.telephony.TelephonyManager');
|
||
var tm = Java.cast(ctx.getSystemService(jStr('phone')), TelephonyManager);
|
||
var line1 = '';
|
||
try { line1 = safeStr(tm.getLine1Number()); } catch (_) {}
|
||
var simOp = '';
|
||
try { simOp = safeStr(tm.getSimOperatorName()); } catch (_) {}
|
||
var simState = -1;
|
||
try { simState = tm.getSimState(); } catch (_) {}
|
||
return {
|
||
success: true,
|
||
phone: line1,
|
||
operator: simOp,
|
||
sim_state: simState,
|
||
found: line1.length >= 11,
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, phone: '', error: String(e) };
|
||
}
|
||
},
|
||
|
||
// ==================== H36 公众号 ====================
|
||
|
||
getOfficialAccounts: function (params) {
|
||
var limit = (params && params.limit) || 100;
|
||
try {
|
||
return Java.performNow(function () {
|
||
var sql = "SELECT username, nickname, conRemark FROM rcontact WHERE username LIKE 'gh_%' ORDER BY nickname LIMIT " + limit;
|
||
var rows = _execSQL(sql);
|
||
return { success: true, accounts: rows.map(function (r) { return { wxid: r.username, name: r.nickname, remark: r.conRemark }; }), count: rows.length };
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), accounts: [] };
|
||
}
|
||
},
|
||
|
||
followOfficialAccount: function (params) {
|
||
var accountId = (params && params.account_id) || '';
|
||
if (!accountId) return { success: false, error: '缺少 account_id' };
|
||
return _intentAction('com.workphone.FOLLOW_OA', { account_id: accountId }, 'oa_followed', { account_id: accountId });
|
||
},
|
||
|
||
unfollowOfficialAccount: function (params) {
|
||
var accountId = (params && params.account_id) || '';
|
||
if (!accountId) return { success: false, error: '缺少 account_id' };
|
||
return _intentAction('com.workphone.UNFOLLOW_OA', { account_id: accountId }, 'oa_unfollowed', { account_id: accountId });
|
||
},
|
||
|
||
getOfficialAccountArticles: function (params) {
|
||
var accountId = (params && params.account_id) || '';
|
||
var limit = (params && params.limit) || 10;
|
||
if (!accountId) return { success: false, error: '缺少 account_id' };
|
||
return _intentAction('com.workphone.GET_OA_ARTICLES', { account_id: accountId, limit: String(limit) }, null, {});
|
||
},
|
||
|
||
// ==================== H37 表情管理 ====================
|
||
|
||
sendEmoji: function (params) {
|
||
var toId = (params && params.to_id) || (params && params.user_id) || '';
|
||
var emojiMd5 = (params && params.emoji_md5) || (params && params.emoji_name) || '';
|
||
if (!toId || !emojiMd5) return { success: false, error: '缺少 to_id 或 emoji_md5' };
|
||
return _intentAction('com.workphone.SEND_EMOJI', { to_id: toId, emoji_md5: emojiMd5 }, 'emoji_sent', { to_id: toId });
|
||
},
|
||
|
||
addCustomEmoji: function (params) {
|
||
var imagePath = (params && params.image_path) || '';
|
||
if (!imagePath) return { success: false, error: '缺少 image_path' };
|
||
return _intentAction('com.workphone.ADD_EMOJI', { image_path: imagePath }, 'emoji_added', {});
|
||
},
|
||
|
||
// ==================== H38 浮窗/多任务 ====================
|
||
|
||
addToFloat: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
return _intentAction('com.workphone.ADD_FLOAT', { wxid: wxid }, 'float_added', { wxid: wxid });
|
||
},
|
||
|
||
removeFromFloat: function (params) {
|
||
var wxid = (params && params.wxid) || '';
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
return _intentAction('com.workphone.REMOVE_FLOAT', { wxid: wxid }, 'float_removed', { wxid: wxid });
|
||
},
|
||
|
||
// ==================== H39 设备信息 ====================
|
||
|
||
getDeviceInfo: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var Build = Java.use('android.os.Build');
|
||
var VERSION = Java.use('android.os.Build$VERSION');
|
||
return {
|
||
success: true,
|
||
device: {
|
||
model: safeStr(Build.MODEL.value),
|
||
brand: safeStr(Build.BRAND.value),
|
||
manufacturer: safeStr(Build.MANUFACTURER.value),
|
||
android_version: safeStr(VERSION.RELEASE.value),
|
||
sdk_int: VERSION.SDK_INT.value,
|
||
device: safeStr(Build.DEVICE.value),
|
||
product: safeStr(Build.PRODUCT.value),
|
||
fingerprint: safeStr(Build.FINGERPRINT.value),
|
||
display: safeStr(Build.DISPLAY.value),
|
||
},
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
getStorageInfo: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var StatFs = Java.use('android.os.StatFs');
|
||
var Environment = Java.use('android.os.Environment');
|
||
var stat = StatFs.$new(Environment.getDataDirectory().getPath());
|
||
var totalBytes = stat.getTotalBytes();
|
||
var freeBytes = stat.getFreeBytes();
|
||
return {
|
||
success: true,
|
||
storage: { total_mb: Math.round(totalBytes / 1048576), free_mb: Math.round(freeBytes / 1048576), used_percent: Math.round((1 - freeBytes / totalBytes) * 100) },
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
getNetworkInfo: function () {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var ctx = getCtx();
|
||
var cm = ctx.getSystemService(jStr('connectivity'));
|
||
var ConnectivityManager = Java.use('android.net.ConnectivityManager');
|
||
var mgr = Java.cast(cm, ConnectivityManager);
|
||
var activeNet = mgr.getActiveNetworkInfo();
|
||
if (activeNet && activeNet.isConnected()) {
|
||
return { success: true, network: { type: safeStr(activeNet.getTypeName()), connected: true, detail: safeStr(activeNet.getExtraInfo()) } };
|
||
}
|
||
return { success: true, network: { type: 'none', connected: false } };
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e) };
|
||
}
|
||
},
|
||
|
||
// ==================== 批量执行 ====================
|
||
|
||
batchExecute: function (params) {
|
||
var actions = (params && params.actions) || [];
|
||
if (actions.length === 0) return { success: false, error: '缺少 actions' };
|
||
var results = [];
|
||
for (var i = 0; i < actions.length; i++) {
|
||
var act = actions[i];
|
||
var fn = rpc.exports[act.action];
|
||
if (fn) {
|
||
try { results.push({ action: act.action, result: fn(act.params || {}) }); }
|
||
catch (e) { results.push({ action: act.action, result: { success: false, error: String(e) } }); }
|
||
} else {
|
||
results.push({ action: act.action, result: { success: false, error: '未知操作' } });
|
||
}
|
||
}
|
||
return { success: true, results: results, count: results.length };
|
||
},
|
||
|
||
// ==================== 奥创协议补全(1077-1270 缺口 action)====================
|
||
// 读类走真 DB(_execSQL message 表 · 字段与 revokeMessage/getMessages 同源),禁止臆造成功;
|
||
// 写/操作类走 _intentAction 诚实通道(无接收端返回 success:false,与奥创 APK 协议一致)。
|
||
|
||
// —— 会话/消息读类(真 DB)——
|
||
getConversations: function (params) {
|
||
var limit = (params && params.limit) || 30;
|
||
try {
|
||
return Java.performNow(function () {
|
||
// 每个 talker 取最新一条,近似会话列表(TriggerConversationPushTask 1232)
|
||
var sql = "SELECT talker, MAX(createTime) AS lastTime, COUNT(*) AS msgCount "
|
||
+ "FROM message GROUP BY talker ORDER BY lastTime DESC LIMIT " + parseInt(limit, 10);
|
||
var rows = _execSQL(sql) || [];
|
||
return { success: true, conversations: rows, count: rows.length, source: 'db.message' };
|
||
});
|
||
} catch (e) { return { success: false, error: String(e) }; }
|
||
},
|
||
|
||
getMessageById: function (params) {
|
||
var id = (params && params.msg_svr_id) || '';
|
||
if (!id) return { success: false, error: '缺少 msg_svr_id' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
var _id = String(id).replace(/'/g, "''");
|
||
var sql = "SELECT msgSvrId, msgId, talker, isSend, type, content, createTime "
|
||
+ "FROM message WHERE msgSvrId='" + _id + "' OR msgId='" + _id + "' LIMIT 1";
|
||
var rows = _execSQL(sql) || [];
|
||
if (!rows.length) return { success: false, error: '消息不存在', error_code: 'msg_not_found', msg_svr_id: id };
|
||
return { success: true, message: rows[0], source: 'db.message' };
|
||
});
|
||
} catch (e) { return { success: false, error: String(e) }; }
|
||
},
|
||
|
||
getMsgIdsByTime: function (params) {
|
||
var start = parseInt((params && params.start_time) || 0, 10);
|
||
var end = parseInt((params && params.end_time) || 0, 10) || Date.now();
|
||
try {
|
||
return Java.performNow(function () {
|
||
var sql = "SELECT msgSvrId, createTime FROM message WHERE createTime>=" + start
|
||
+ " AND createTime<=" + end + " ORDER BY createTime DESC LIMIT 500";
|
||
var rows = _execSQL(sql) || [];
|
||
return { success: true, msg_svr_ids: rows, count: rows.length, source: 'db.message' };
|
||
});
|
||
} catch (e) { return { success: false, error: String(e) }; }
|
||
},
|
||
|
||
getMessageRawXml: function (params) {
|
||
var id = (params && params.msg_svr_id) || '';
|
||
if (!id) return { success: false, error: '缺少 msg_svr_id' };
|
||
try {
|
||
return Java.performNow(function () {
|
||
var _id = String(id).replace(/'/g, "''");
|
||
var sql = "SELECT msgSvrId, type, content FROM message WHERE msgSvrId='" + _id + "' OR msgId='" + _id + "' LIMIT 1";
|
||
var rows = _execSQL(sql) || [];
|
||
if (!rows.length) return { success: false, error: '消息不存在', error_code: 'msg_not_found', msg_svr_id: id };
|
||
return { success: true, raw_xml: rows[0].content, type: rows[0].type, msg_svr_id: id, source: 'db.message' };
|
||
});
|
||
} catch (e) { return { success: false, error: String(e) }; }
|
||
},
|
||
|
||
// —— 加好友多场景(写类 · intent 诚实通道)——
|
||
addFriendInChatRoom: function (params) {
|
||
var room = (params && params.group_id) || '';
|
||
var wxid = (params && (params.wxid || params.user_id)) || '';
|
||
if (!room || !wxid) return { success: false, error: '缺少 group_id 或 wxid' };
|
||
return _intentAction('com.workphone.ADD_FRIEND_IN_ROOM', { group_id: room, wxid: wxid, verify: (params && params.verify_message) || '' }, 'add_friend_in_room', { group_id: room, wxid: wxid });
|
||
},
|
||
addFriendFromPhonebook: function (params) {
|
||
var phone = (params && params.phone) || '';
|
||
if (!phone) return { success: false, error: '缺少 phone' };
|
||
return _intentAction('com.workphone.ADD_FRIEND_PHONEBOOK', { phone: phone, verify: (params && params.verify_message) || '' }, 'add_friend_phonebook', { phone: phone });
|
||
},
|
||
addFriendWithScene: function (params) {
|
||
var wxid = (params && (params.wxid || params.user_id)) || '';
|
||
var scene = (params && params.scene) || '30';
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
return _intentAction('com.workphone.ADD_FRIEND_SCENE', { wxid: wxid, scene: String(scene), verify: (params && params.verify_message) || '' }, 'add_friend_scene', { wxid: wxid, scene: scene });
|
||
},
|
||
addFriendByCard: function (params) {
|
||
var card = (params && (params.card_wxid || params.wxid)) || '';
|
||
if (!card) return { success: false, error: '缺少 card_wxid' };
|
||
return _intentAction('com.workphone.ADD_FRIEND_CARD', { card_wxid: card, verify: (params && params.verify_message) || '' }, 'add_friend_card', { card_wxid: card });
|
||
},
|
||
joinGroupByQr: function (params) {
|
||
var qr = (params && (params.qr_content || params.image_url)) || '';
|
||
if (!qr) return { success: false, error: '缺少 qr_content 或 image_url' };
|
||
return _intentAction('com.workphone.JOIN_GROUP_QR', { qr: qr }, 'join_group_qr', { qr: qr });
|
||
},
|
||
|
||
// —— 群管理补(写类 · intent)——
|
||
approveGroupInvite: function (params) {
|
||
var room = (params && params.group_id) || '';
|
||
var wxid = (params && (params.wxid || params.user_id)) || '';
|
||
if (!room) return { success: false, error: '缺少 group_id' };
|
||
return _intentAction('com.workphone.APPROVE_GROUP_INVITE', { group_id: room, wxid: wxid }, 'approve_group_invite', { group_id: room, wxid: wxid });
|
||
},
|
||
agreeJoinGroup: function (params) {
|
||
var room = (params && (params.group_id || params.invite_url)) || '';
|
||
if (!room) return { success: false, error: '缺少 group_id 或 invite_url' };
|
||
return _intentAction('com.workphone.AGREE_JOIN_GROUP', { group_id: room }, 'agree_join_group', { group_id: room });
|
||
},
|
||
|
||
// —— 聊天补(写类 · intent)——
|
||
markConversationRead: function (params) {
|
||
var talker = (params && (params.conversation_id || params.user_id || params.talker)) || '';
|
||
if (!talker) return { success: false, error: '缺少 conversation_id' };
|
||
return _intentAction('com.workphone.MARK_READ', { talker: talker }, 'conversation_read', { talker: talker });
|
||
},
|
||
sendMultiImage: function (params) {
|
||
var talker = (params && (params.to_id || params.user_id)) || '';
|
||
var images = (params && params.images) || [];
|
||
if (!talker || !images.length) return { success: false, error: '缺少 to_id 或 images' };
|
||
return _intentAction('com.workphone.SEND_MULTI_IMAGE', { to_id: talker, images: images.join(',') }, 'send_multi_image', { to_id: talker, count: images.length });
|
||
},
|
||
sendJielong: function (params) {
|
||
var talker = (params && (params.to_id || params.group_id)) || '';
|
||
var content = (params && params.content) || '';
|
||
if (!talker || !content) return { success: false, error: '缺少 to_id 或 content' };
|
||
return _intentAction('com.workphone.SEND_JIELONG', { to_id: talker, content: content }, 'send_jielong', { to_id: talker });
|
||
},
|
||
sendFriendVerify: function (params) {
|
||
var wxid = (params && (params.wxid || params.user_id)) || '';
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
return _intentAction('com.workphone.SEND_FRIEND_VERIFY', { wxid: wxid, verify: (params && params.verify_message) || '' }, 'send_friend_verify', { wxid: wxid });
|
||
},
|
||
voiceToText: function (params) {
|
||
var id = (params && params.msg_svr_id) || '';
|
||
if (!id) return { success: false, error: '缺少 msg_svr_id' };
|
||
return _intentAction('com.workphone.VOICE_TO_TEXT', { msg_svr_id: id }, 'voice_to_text', { msg_svr_id: id });
|
||
},
|
||
|
||
// —— 朋友圈补(写/读混合 · intent 诚实)——
|
||
getFriendMoments: function (params) {
|
||
var wxid = (params && (params.wxid || params.user_id)) || '';
|
||
if (!wxid) return { success: false, error: '缺少 wxid' };
|
||
return _intentAction('com.workphone.GET_FRIEND_MOMENTS', { wxid: wxid }, 'get_friend_moments', { wxid: wxid });
|
||
},
|
||
getMomentDetail: function (params) {
|
||
var sid = (params && params.sns_id) || '';
|
||
if (!sid) return { success: false, error: '缺少 sns_id' };
|
||
return _intentAction('com.workphone.GET_MOMENT_DETAIL', { sns_id: sid }, 'get_moment_detail', { sns_id: sid });
|
||
},
|
||
deleteMomentComment: function (params) {
|
||
var sid = (params && params.sns_id) || '';
|
||
var cid = (params && params.comment_id) || '';
|
||
if (!sid) return { success: false, error: '缺少 sns_id' };
|
||
return _intentAction('com.workphone.DEL_MOMENT_COMMENT', { sns_id: sid, comment_id: cid }, 'del_moment_comment', { sns_id: sid });
|
||
},
|
||
replyMomentComment: function (params) {
|
||
var sid = (params && params.sns_id) || '';
|
||
var content = (params && params.content) || '';
|
||
if (!sid || !content) return { success: false, error: '缺少 sns_id 或 content' };
|
||
return _intentAction('com.workphone.REPLY_MOMENT_COMMENT', { sns_id: sid, content: content }, 'reply_moment_comment', { sns_id: sid });
|
||
},
|
||
stopMomentsPraise: function () {
|
||
return _intentAction('com.workphone.STOP_MOMENTS_PRAISE', {}, 'stop_moments_praise', {});
|
||
},
|
||
|
||
// —— 养号/清粉(写类 · intent)——
|
||
startNurture: function (params) {
|
||
return _intentAction('com.workphone.START_NURTURE', { plan: (params && params.plan) || 'default' }, 'nurture_started', params || {});
|
||
},
|
||
stopNurture: function () {
|
||
return _intentAction('com.workphone.STOP_NURTURE', {}, 'nurture_stopped', {});
|
||
},
|
||
detectZombieFans: function () {
|
||
return _intentAction('com.workphone.DETECT_ZOMBIE', {}, 'detect_zombie_started', {});
|
||
},
|
||
stopDetectZombieFans: function () {
|
||
return _intentAction('com.workphone.STOP_DETECT_ZOMBIE', {}, 'detect_zombie_stopped', {});
|
||
},
|
||
|
||
// —— 查询类(写类 · intent 诚实)——
|
||
queryRedPacket: function (params) {
|
||
var id = (params && (params.red_packet_id || params.msg_svr_id)) || '';
|
||
if (!id) return { success: false, error: '缺少 red_packet_id' };
|
||
return _intentAction('com.workphone.QUERY_RED_PACKET', { red_packet_id: id }, 'query_red_packet', { red_packet_id: id });
|
||
},
|
||
getWechatLocation: function () {
|
||
return _intentAction('com.workphone.GET_WX_LOCATION', {}, 'wechat_location', {});
|
||
},
|
||
searchOfficialAccount: function (params) {
|
||
var kw = (params && params.keyword) || '';
|
||
if (!kw) return { success: false, error: '缺少 keyword' };
|
||
return _intentAction('com.workphone.SEARCH_BIZ', { keyword: kw }, 'search_biz', { keyword: kw });
|
||
},
|
||
|
||
// —— 手机操作(写类 · intent)——
|
||
phoneAction: function (params) {
|
||
var act = (params && params.op) || '';
|
||
if (!act) return { success: false, error: '缺少 op(reboot/lock/screenshot 等)' };
|
||
return _intentAction('com.workphone.PHONE_ACTION', { op: act }, 'phone_action', { op: act });
|
||
},
|
||
};
|
||
|
||
// ============================================================
|
||
// § 5 内部实现 — 消息发送 (H17)
|
||
// ============================================================
|
||
|
||
function _sendMessageInternal(toId, content, msgType) {
|
||
var sent = false;
|
||
var sendMethod = 'failed';
|
||
var intentFailure = null;
|
||
var eventFailure = null;
|
||
var verifiedMessage = null;
|
||
|
||
// 方案 A: 直接调用微信内部消息发送类(按版本兼容表自动选择)
|
||
var candidateClasses = getCompat('send_message');
|
||
|
||
for (var i = 0; i < candidateClasses.length && !sent; i++) {
|
||
try {
|
||
var c = candidateClasses[i];
|
||
var MsgCls = Java.use(c.cls);
|
||
if (MsgCls) {
|
||
var m = c.sig ? MsgCls[c.method].overload.apply(MsgCls[c.method], c.sig) : MsgCls[c.method];
|
||
if (c.sig && c.sig.length === 3) {
|
||
m.call(MsgCls, jStr(toId), jStr(content), jStr(msgType === 'text' ? '' : msgType));
|
||
} else {
|
||
m.call(MsgCls, jStr(toId), jStr(content));
|
||
}
|
||
sent = true;
|
||
sendMethod = 'internal_' + c.cls.split('.').pop();
|
||
log('info', 'send', '消息通过内部方法发送: ' + c.cls);
|
||
emitEvent('message_sent', { to_id: toId, content: content, msg_type: msgType, method: sendMethod });
|
||
}
|
||
} catch (_) { continue; }
|
||
}
|
||
|
||
// 方案 B: WeChat 8.0.69 已验证存在 SendMsgEvent;发布后必须 DB 回查命中,才算成功。
|
||
if (!sent && (msgType || 'text') === 'text') {
|
||
var eventResult = _sendMessageBySendMsgEvent(toId, content);
|
||
if (eventResult && eventResult.success && eventResult.verified) {
|
||
sent = true;
|
||
sendMethod = eventResult.method || 'send_msg_event';
|
||
verifiedMessage = eventResult.message || null;
|
||
log('info', 'send', '消息通过 SendMsgEvent 发送并 DB 验证');
|
||
emitEvent('message_sent', {
|
||
to_id: toId,
|
||
content: content,
|
||
msg_type: msgType,
|
||
method: sendMethod,
|
||
message_id: eventResult.message_id
|
||
});
|
||
} else {
|
||
eventFailure = eventResult || { success: false, error: 'send_msg_event_failed' };
|
||
log('warn', 'send', 'SendMsgEvent 未验证: ' + safeStr(eventFailure.error));
|
||
}
|
||
}
|
||
|
||
// 方案 C: Intent 广播降级;无真实接收端时不得把广播计为发送成功。
|
||
if (!sent) {
|
||
var intentResult = _intentAction(
|
||
'com.workphone.SEND_MESSAGE',
|
||
{ to_id: toId, content: content, msg_type: msgType },
|
||
'message_sent',
|
||
{ to_id: toId, content: content, msg_type: msgType, method: 'intent_broadcast' }
|
||
);
|
||
if (intentResult && intentResult.success) {
|
||
sent = true;
|
||
sendMethod = 'intent_broadcast';
|
||
log('info', 'send', '消息通过 Intent 广播发送');
|
||
} else {
|
||
intentFailure = intentResult || { success: false, error: 'intent_dispatch_failed' };
|
||
log('warn', 'send', 'Intent 发送未验证: ' + safeStr(intentFailure.error));
|
||
}
|
||
}
|
||
|
||
var messageId = sent ? ((verifiedMessage && verifiedMessage.id) || generateId('msg')) : null;
|
||
if (sent) {
|
||
var talker = (toId === '文件传输助手' || toId === 'File Transfer') ? 'filehelper' : toId;
|
||
_messageBuffer.push({
|
||
msg_svr_id: messageId,
|
||
msg_type: msgType || 'text',
|
||
from_id: talker,
|
||
content: content,
|
||
timestamp: new Date().toISOString(),
|
||
is_send: true,
|
||
});
|
||
if (_messageBuffer.length > CONFIG.MESSAGE_BUFFER_MAX) {
|
||
_messageBuffer = _messageBuffer.slice(-Math.floor(CONFIG.MESSAGE_BUFFER_MAX * 0.6));
|
||
}
|
||
}
|
||
|
||
var result = {
|
||
success: sent,
|
||
message_id: messageId,
|
||
method: sendMethod,
|
||
verified: sent,
|
||
to_id: toId,
|
||
};
|
||
if (!sent && intentFailure) {
|
||
result.error = intentFailure.error || 'send_failed';
|
||
result.action = intentFailure.action || 'com.workphone.SEND_MESSAGE';
|
||
result.hint = intentFailure.hint || '';
|
||
if (eventFailure) {
|
||
result.event_error = eventFailure.error || 'send_msg_event_failed';
|
||
result.event_published = !!eventFailure.published;
|
||
result.event_verified = !!eventFailure.verified;
|
||
result.event_method = eventFailure.method || 'send_msg_event';
|
||
}
|
||
} else if (!sent && eventFailure) {
|
||
result.error = eventFailure.error || 'send_msg_event_failed';
|
||
result.event_published = !!eventFailure.published;
|
||
result.event_verified = !!eventFailure.verified;
|
||
result.event_method = eventFailure.method || 'send_msg_event';
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function _normalizeTalkerId(toId) {
|
||
if (toId === '文件传输助手' || toId === 'File Transfer') return 'filehelper';
|
||
return toId;
|
||
}
|
||
|
||
function _calcSendMsgScene(toId) {
|
||
try {
|
||
var A2 = Java.use('vn0.a2');
|
||
return A2.y(jStr(toId));
|
||
} catch (_) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
function _sendMessageBySendMsgEvent(toId, content) {
|
||
var published = false;
|
||
var scene = 0;
|
||
try {
|
||
var SendMsgEvent = Java.use('com.tencent.mm.autogen.events.SendMsgEvent');
|
||
var IEvent = Java.use('com.tencent.mm.sdk.event.IEvent');
|
||
var event = SendMsgEvent.$new();
|
||
var payload = event.g.value;
|
||
scene = _calcSendMsgScene(toId);
|
||
payload.a.value = jStr(toId);
|
||
payload.b.value = jStr(content);
|
||
payload.c.value = scene;
|
||
payload.d.value = 0;
|
||
published = !!IEvent.e.overload().call(event);
|
||
var verify = _findSentMessageInDB(toId, content, 3000);
|
||
if (verify.success) {
|
||
return {
|
||
success: true,
|
||
method: 'send_msg_event',
|
||
published: published,
|
||
verified: true,
|
||
message_id: verify.message_id,
|
||
message: verify.message,
|
||
scene: scene
|
||
};
|
||
}
|
||
return {
|
||
success: false,
|
||
method: 'send_msg_event',
|
||
published: published,
|
||
verified: false,
|
||
scene: scene,
|
||
error: verify.error || 'db_verify_timeout',
|
||
verify: verify
|
||
};
|
||
} catch (e) {
|
||
return {
|
||
success: false,
|
||
method: 'send_msg_event',
|
||
published: published,
|
||
verified: false,
|
||
scene: scene,
|
||
error: String(e)
|
||
};
|
||
}
|
||
}
|
||
|
||
function _verifyMessageRevokedInDB(msgSvrId, timeoutMs) {
|
||
var _id = String(msgSvrId).replace(/'/g, "''");
|
||
var deadline = Date.now() + (timeoutMs || 5000);
|
||
var Thread = null;
|
||
try { Thread = Java.use('java.lang.Thread'); } catch (_) {}
|
||
do {
|
||
var sql = "SELECT type, content FROM message WHERE msgSvrId = '" + _id
|
||
+ "' OR msgId = '" + _id + "' LIMIT 1";
|
||
var rows = _execSQL(sql);
|
||
if (rows && rows.length > 0) {
|
||
var t = String(rows[0].type);
|
||
var c = rows[0].content || '';
|
||
if (t === '10000' || c.indexOf('撤回') !== -1 || c.indexOf('recalled') !== -1) {
|
||
return { success: true, verified: true, type: t, content_preview: String(c).substring(0, 80) };
|
||
}
|
||
}
|
||
if (Thread) Thread.sleep(300);
|
||
else Java.use('java.lang.Thread').sleep(300);
|
||
} while (Date.now() < deadline);
|
||
return { success: false, verified: false, error: 'revoke_db_verify_timeout' };
|
||
}
|
||
|
||
function _revokeMessageByRevokeMsgEvent(msgSvrId, row) {
|
||
var published = false;
|
||
try {
|
||
var RevokeMsgEvent = Java.use('com.tencent.mm.autogen.events.RevokeMsgEvent');
|
||
var IEvent = Java.use('com.tencent.mm.sdk.event.IEvent');
|
||
var event = RevokeMsgEvent.$new();
|
||
var payload = event.g.value;
|
||
var talker = (row && row.talker) ? String(row.talker) : '';
|
||
var svrId = (row && row.msgSvrId) ? String(row.msgSvrId) : String(msgSvrId);
|
||
var localId = (row && row.msgId) ? String(row.msgId) : String(msgSvrId);
|
||
var createTime = parseInt((row && row.createTime) || '0', 10) || 0;
|
||
// autogen payload 字段(8.0.69):a=talker b=msgSvrId(long) c=createTime d=msgId
|
||
try { if (payload.a !== undefined) payload.a.value = jStr(talker); } catch (_) {}
|
||
try { if (payload.b !== undefined) payload.b.value = jLong(svrId); } catch (_) {}
|
||
try { if (payload.c !== undefined) payload.c.value = createTime; } catch (_) {}
|
||
try { if (payload.d !== undefined) payload.d.value = jLong(localId); } catch (_) {}
|
||
published = !!IEvent.e.overload().call(event);
|
||
var verify = _verifyMessageRevokedInDB(msgSvrId, 5000);
|
||
if (verify.verified) {
|
||
return {
|
||
success: true,
|
||
method: 'revoke_msg_event',
|
||
published: published,
|
||
verified: true,
|
||
msg_svr_id: msgSvrId,
|
||
talker: talker,
|
||
verify: verify,
|
||
channel: 'hook'
|
||
};
|
||
}
|
||
return {
|
||
success: false,
|
||
method: 'revoke_msg_event',
|
||
published: published,
|
||
verified: false,
|
||
msg_svr_id: msgSvrId,
|
||
talker: talker,
|
||
error: verify.error || 'revoke_not_confirmed_in_db',
|
||
payload_class: safeStr(payload.getClass().getName())
|
||
};
|
||
} catch (e) {
|
||
return {
|
||
success: false,
|
||
method: 'revoke_msg_event',
|
||
published: published,
|
||
verified: false,
|
||
error: String(e)
|
||
};
|
||
}
|
||
}
|
||
|
||
function _findSentMessageInDB(toId, content, timeoutMs) {
|
||
var talker = _normalizeTalkerId(toId);
|
||
var deadline = Date.now() + (timeoutMs || 2000);
|
||
var Thread = null;
|
||
try { Thread = Java.use('java.lang.Thread'); } catch (_) {}
|
||
var sql = "SELECT msgId AS id, type, talker, content, createTime, isSend FROM message"
|
||
+ " WHERE talker='" + _escapeSql(talker) + "'"
|
||
+ " AND isSend=1"
|
||
+ " AND content='" + _escapeSql(content) + "'"
|
||
+ " ORDER BY createTime DESC LIMIT 1";
|
||
|
||
do {
|
||
var rows = _execSQL(sql);
|
||
if (rows && rows.length > 0) {
|
||
return {
|
||
success: true,
|
||
message_id: rows[0].id || rows[0].msgId || null,
|
||
message: rows[0],
|
||
talker: talker
|
||
};
|
||
}
|
||
if (Date.now() >= deadline) break;
|
||
if (Thread) Thread.sleep(250);
|
||
} while (Date.now() < deadline);
|
||
|
||
return {
|
||
success: false,
|
||
error: 'db_verify_timeout',
|
||
talker: talker,
|
||
diag: _lastDbDiag || {}
|
||
};
|
||
}
|
||
|
||
// ============================================================
|
||
// § 6 内部实现 — 数据库读取 (H15/H16)
|
||
// ============================================================
|
||
|
||
function _getMessagesFromDB(conversationId, limit, offset) {
|
||
limit = parseInt(limit, 10) || 3000;
|
||
if (limit < 1) limit = 3000;
|
||
if (limit > 5000) limit = 5000;
|
||
offset = parseInt(offset, 10) || 0;
|
||
if (offset < 0) offset = 0;
|
||
// DB talker 用 wxid;显示名「文件传输助手」→ filehelper
|
||
if (conversationId === '文件传输助手' || conversationId === 'File Transfer') {
|
||
conversationId = 'filehelper';
|
||
}
|
||
var where = conversationId
|
||
? " WHERE talker='" + conversationId.replace(/'/g, "''") + "'"
|
||
: '';
|
||
var sql = 'SELECT msgId, type, talker, content, createTime, isSend FROM message'
|
||
+ where + ' ORDER BY createTime DESC LIMIT ' + limit + ' OFFSET ' + offset;
|
||
var rows = _execSQL(sql);
|
||
var totalCount = null;
|
||
try {
|
||
var cntRows = _execSQL('SELECT count(*) AS n FROM message' + where);
|
||
totalCount = cntRows && cntRows[0] ? parseInt(cntRows[0].n, 10) : null;
|
||
} catch (e) { /* ignore count failure */ }
|
||
return {
|
||
success: true,
|
||
messages: rows.map(function (r) {
|
||
return {
|
||
id: r.msgId, type: r.type, from_id: r.talker,
|
||
content: r.content, timestamp: r.createTime,
|
||
is_send: r.isSend === '1',
|
||
};
|
||
}),
|
||
count: rows.length,
|
||
returned_count: rows.length,
|
||
total_count: totalCount,
|
||
requested_limit: limit,
|
||
offset: offset,
|
||
has_more: totalCount !== null ? totalCount > offset + rows.length : rows.length >= limit,
|
||
};
|
||
}
|
||
|
||
function _getContactsFromDB(limit, offset) {
|
||
limit = parseInt(limit, 10) || 10000;
|
||
if (limit < 1) limit = 10000;
|
||
if (limit > 20000) limit = 20000;
|
||
offset = parseInt(offset, 10) || 0;
|
||
if (offset < 0) offset = 0;
|
||
var labelMap = {};
|
||
try {
|
||
var labelRows = _execSQL("SELECT labelId, labelName FROM contactlabelinfo");
|
||
labelRows.forEach(function (r) {
|
||
labelMap[r.labelId] = r.labelName;
|
||
});
|
||
} catch (e) { /* 部分版本无标签表 */ }
|
||
|
||
var contactLabelMap = {};
|
||
try {
|
||
// 8.0.69 列名为 contactLabelIds(旧版 labelidlist)
|
||
var clRows = _execSQL(
|
||
"SELECT username, contactLabelIds FROM rcontact WHERE contactLabelIds IS NOT NULL AND contactLabelIds != ''"
|
||
);
|
||
clRows.forEach(function (r) {
|
||
if (!r.contactLabelIds) return;
|
||
var ids = String(r.contactLabelIds).split(",").filter(function (id) { return id; });
|
||
contactLabelMap[r.username] = ids.map(function (id) {
|
||
return labelMap[id] || id;
|
||
}).filter(function (n) { return n; });
|
||
});
|
||
} catch (e) { /* ignore */ }
|
||
|
||
// 8.0.69 兼容:先探测 rcontact 实际列,只 SELECT 存在的列(避免缺列如 signature/country 致整查询返 0)
|
||
var _wantCols = ["username", "nickname", "conRemark", "type", "alias", "imgFlag", "sex", "country", "province", "city", "signature"];
|
||
var _availCols = {};
|
||
try {
|
||
var _ti = _execSQL("PRAGMA table_info(rcontact)");
|
||
(_ti || []).forEach(function (c) { if (c && c.name) _availCols[c.name] = true; });
|
||
} catch (ept) { /* PRAGMA 不支持时退回全列 */ }
|
||
var _useCols = Object.keys(_availCols).length > 0
|
||
? _wantCols.filter(function (c) { return _availCols[c]; })
|
||
: _wantCols;
|
||
if (_useCols.indexOf("username") < 0) _useCols.unshift("username");
|
||
var _cols = "SELECT " + _useCols.join(", ") + " FROM rcontact ";
|
||
var _excludeSys = "username NOT LIKE '%@chatroom' AND username NOT LIKE 'gh_%' "
|
||
+ "AND username NOT IN ('filehelper','fmessage','medianote','floatbottle','newsapp','weixin','qmessage','tmessage') ";
|
||
var page = " LIMIT " + limit + " OFFSET " + offset;
|
||
var sql = _cols + "WHERE type NOT IN (0, 33) AND verifyFlag=0 AND " + _excludeSys
|
||
+ "ORDER BY conRemark, nickname" + page;
|
||
var rows = _execSQL(sql);
|
||
var _diag = { strict: (rows || []).length, use_cols: _useCols, avail_cols: Object.keys(_availCols), requested_limit: limit, offset: offset };
|
||
try {
|
||
var rawCnt = _execSQL("SELECT count(*) AS n FROM rcontact");
|
||
_diag.rcontact_total = rawCnt && rawCnt[0] ? parseInt(rawCnt[0].n, 10) : null;
|
||
} catch (e0a) { _diag.rcontact_total_err = String(e0a); }
|
||
try {
|
||
var filteredCnt = _execSQL("SELECT count(*) AS n FROM rcontact WHERE type NOT IN (0, 33) AND verifyFlag=0 AND " + _excludeSys);
|
||
_diag.filtered_total = filteredCnt && filteredCnt[0] ? parseInt(filteredCnt[0].n, 10) : null;
|
||
} catch (e0b) { _diag.filtered_total_err = String(e0b); }
|
||
// 8.0.69 兼容回退①:严格过滤(type/verifyFlag)为空时,放宽为仅排除系统/群/公众号
|
||
if (!rows || rows.length === 0) {
|
||
var sqlRelax = _cols + "WHERE verifyFlag=0 AND " + _excludeSys
|
||
+ "ORDER BY conRemark, nickname" + page;
|
||
var relaxed = _execSQL(sqlRelax);
|
||
_diag.relax_verifyflag = (relaxed || []).length;
|
||
if (relaxed && relaxed.length > 0) {
|
||
log('info', 'contacts', '严格过滤空,回退宽过滤命中 ' + relaxed.length + ' 条');
|
||
rows = relaxed;
|
||
}
|
||
}
|
||
// 8.0.69 兼容回退②:连 verifyFlag=0 也空时,丢弃 verifyFlag,仅排除系统/群/公众号(部分版本好友 verifyFlag!=0)
|
||
if (!rows || rows.length === 0) {
|
||
var sqlNoVerify = _cols + "WHERE " + _excludeSys + "ORDER BY conRemark, nickname" + page;
|
||
var noverify = _execSQL(sqlNoVerify);
|
||
_diag.relax_noverify = (noverify || []).length;
|
||
if (noverify && noverify.length > 0) {
|
||
log('info', 'contacts', 'verifyFlag 过滤空,丢弃 verifyFlag 命中 ' + noverify.length + ' 条');
|
||
rows = noverify;
|
||
}
|
||
}
|
||
// 诊断:rcontact 原始总行数 + 字段探测(定位 8.0.69 读空根因,写入返回的 _diag)
|
||
if (!rows || rows.length === 0) {
|
||
try { var rawCnt = _execSQL("SELECT count(*) AS n FROM rcontact"); _diag.rcontact_total = rawCnt && rawCnt[0] ? rawCnt[0].n : null; } catch (e0) { _diag.rcontact_total_err = String(e0); }
|
||
try { var vf = _execSQL("SELECT verifyFlag, count(*) AS n FROM rcontact GROUP BY verifyFlag"); _diag.verifyflag_dist = vf; } catch (e1) { _diag.verifyflag_err = String(e1); }
|
||
try { var tp = _execSQL("SELECT type, count(*) AS n FROM rcontact GROUP BY type LIMIT 20"); _diag.type_dist = tp; } catch (e2) { _diag.type_err = String(e2); }
|
||
}
|
||
var sexMap = { "0": "unknown", "1": "male", "2": "female" };
|
||
var contacts = rows.map(function (r) {
|
||
var nick = r.nickname || "";
|
||
var remark = r.conRemark || "";
|
||
var alias = r.alias || "";
|
||
var wxid = r.username || "";
|
||
var display = nick || remark || alias || wxid;
|
||
return {
|
||
wxid: wxid,
|
||
user_id: wxid,
|
||
nickname: nick,
|
||
display_name: display,
|
||
remark: remark,
|
||
alias: alias,
|
||
wechat_id: alias,
|
||
type: r.type,
|
||
sex: sexMap[String(r.sex)] || "unknown",
|
||
country: r.country || "",
|
||
province: r.province || "",
|
||
city: r.city || "",
|
||
signature: r.signature || "",
|
||
tags: contactLabelMap[wxid] || [],
|
||
has_avatar: (r.imgFlag || 0) > 0,
|
||
};
|
||
});
|
||
_contactCache = contacts;
|
||
return {
|
||
success: true,
|
||
contacts: contacts,
|
||
count: contacts.length,
|
||
returned_count: contacts.length,
|
||
total_count: _diag.filtered_total,
|
||
raw_total_count: _diag.rcontact_total,
|
||
requested_limit: limit,
|
||
offset: offset,
|
||
has_more: _diag.filtered_total !== null && _diag.filtered_total !== undefined
|
||
? _diag.filtered_total > offset + contacts.length
|
||
: contacts.length >= limit,
|
||
_diag: _diag
|
||
};
|
||
}
|
||
|
||
function _getContactInfoFromDB(wxid) {
|
||
var sql = "SELECT username, nickname, conRemark, type, alias FROM rcontact"
|
||
+ " WHERE username='" + wxid.replace(/'/g, "''") + "'";
|
||
var rows = _execSQL(sql);
|
||
if (rows.length > 0) return { success: true, contact: rows[0] };
|
||
return { success: false, error: '未找到联系人: ' + wxid };
|
||
}
|
||
|
||
// ============================================================
|
||
// § 7 内部实现 — 好友管理 (H18/H19)
|
||
// ============================================================
|
||
|
||
function _getFriendRequestsFromDB(limit) {
|
||
var sql = "SELECT msgId, type, talker, content, createTime FROM message"
|
||
+ " WHERE type = 10002"
|
||
+ " ORDER BY createTime DESC LIMIT " + limit;
|
||
var rows = _execSQL(sql);
|
||
var requests = rows.map(function (r) {
|
||
var info = _parseFriendRequestXml(r.content);
|
||
return {
|
||
msg_id: r.msgId,
|
||
from_id: info.fromusername || r.talker,
|
||
from_nickname: info.fromnickname || '',
|
||
content: info.content || '',
|
||
encrypt_username: info.encryptusername || '',
|
||
ticket: info.ticket || '',
|
||
source: info.sourceType || '',
|
||
timestamp: r.createTime,
|
||
};
|
||
});
|
||
return { success: true, requests: requests, count: requests.length };
|
||
}
|
||
|
||
function _parseFriendRequestXml(xml) {
|
||
var info = {};
|
||
if (!xml) return info;
|
||
var fields = ['fromusername', 'fromnickname', 'content', 'encryptusername', 'ticket', 'sourceType'];
|
||
for (var i = 0; i < fields.length; i++) {
|
||
var re = new RegExp(fields[i] + '="([^"]*)"');
|
||
var m = xml.match(re);
|
||
if (m) info[fields[i]] = m[1];
|
||
}
|
||
return info;
|
||
}
|
||
|
||
function _addFriendInternal(userId, message, sourceType) {
|
||
return _intentAction(
|
||
'com.workphone.ADD_FRIEND',
|
||
{ user_id: userId, message: message, source_type: sourceType },
|
||
'friend_request_sent',
|
||
{ user_id: userId, message: message, source_type: sourceType }
|
||
);
|
||
}
|
||
|
||
function _acceptFriendInternal(encryptUserName, ticket) {
|
||
return _intentAction(
|
||
'com.workphone.ACCEPT_FRIEND',
|
||
{ encrypt_username: encryptUserName, ticket: ticket },
|
||
'friend_accepted',
|
||
{ encrypt_username: encryptUserName }
|
||
);
|
||
}
|
||
|
||
function _deleteFriendInternal(wxid) {
|
||
return _intentAction(
|
||
'com.workphone.DELETE_FRIEND',
|
||
{ wxid: wxid },
|
||
'friend_deleted',
|
||
{ wxid: wxid }
|
||
);
|
||
}
|
||
|
||
function _setRemarkInternal(wxid, remark) {
|
||
return _intentAction('com.workphone.SET_REMARK', { wxid: wxid, remark: remark });
|
||
}
|
||
|
||
// ============================================================
|
||
// § 8 内部实现 — 群管理 (H22)
|
||
// ============================================================
|
||
|
||
function _getGroupsFromDB(limit, offset) {
|
||
limit = parseInt(limit, 10) || 100;
|
||
if (limit < 1) limit = 100;
|
||
if (limit > 5000) limit = 5000;
|
||
offset = parseInt(offset, 10) || 0;
|
||
if (offset < 0) offset = 0;
|
||
var sql = "SELECT username, nickname FROM rcontact"
|
||
+ " WHERE username LIKE '%@chatroom'"
|
||
+ " ORDER BY nickname LIMIT " + limit + " OFFSET " + offset;
|
||
var rows = _execSQL(sql);
|
||
var totalCount = null;
|
||
try {
|
||
var cntRows = _execSQL("SELECT count(*) AS n FROM rcontact WHERE username LIKE '%@chatroom'");
|
||
totalCount = cntRows && cntRows[0] ? parseInt(cntRows[0].n, 10) : null;
|
||
} catch (e) { /* ignore count failure */ }
|
||
return {
|
||
success: true,
|
||
groups: rows.map(function (r) {
|
||
return { group_id: r.username, name: r.nickname };
|
||
}),
|
||
count: rows.length,
|
||
returned_count: rows.length,
|
||
total_count: totalCount,
|
||
requested_limit: limit,
|
||
offset: offset,
|
||
has_more: totalCount !== null ? totalCount > offset + rows.length : rows.length >= limit,
|
||
};
|
||
}
|
||
|
||
function _getLabelsFromDB(limit, offset) {
|
||
limit = parseInt(limit, 10) || 5000;
|
||
if (limit < 1) limit = 5000;
|
||
if (limit > 20000) limit = 20000;
|
||
offset = parseInt(offset, 10) || 0;
|
||
if (offset < 0) offset = 0;
|
||
|
||
var tables = [
|
||
{ table: "contactlabel", id: "labelID", name: "labelName" },
|
||
{ table: "contactlabelinfo", id: "labelId", name: "labelName" }
|
||
];
|
||
var rows = [];
|
||
var totalCount = null;
|
||
var sourceTable = "";
|
||
for (var i = 0; i < tables.length; i++) {
|
||
var t = tables[i];
|
||
try {
|
||
rows = _execSQL(
|
||
"SELECT " + t.id + " AS labelID, " + t.name + " AS labelName FROM " + t.table
|
||
+ " ORDER BY CAST(" + t.id + " AS INTEGER), " + t.id
|
||
+ " LIMIT " + limit + " OFFSET " + offset
|
||
);
|
||
var cntRows = _execSQL("SELECT count(*) AS n FROM " + t.table);
|
||
totalCount = cntRows && cntRows[0] ? parseInt(cntRows[0].n, 10) : null;
|
||
sourceTable = t.table;
|
||
break;
|
||
} catch (e) {
|
||
rows = [];
|
||
totalCount = null;
|
||
}
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
labels: rows.map(function (r) { return { id: r.labelID || r.labelId, name: r.labelName || "" }; }),
|
||
count: rows.length,
|
||
returned_count: rows.length,
|
||
total_count: totalCount,
|
||
requested_limit: limit,
|
||
offset: offset,
|
||
has_more: totalCount !== null ? totalCount > offset + rows.length : rows.length >= limit,
|
||
source_table: sourceTable,
|
||
};
|
||
}
|
||
|
||
function _getGroupMembersFromDB(groupId) {
|
||
var sql = "SELECT memberlist, displayname FROM chatroom"
|
||
+ " WHERE chatroomname='" + groupId.replace(/'/g, "''") + "'";
|
||
var rows = _execSQL(sql);
|
||
if (rows.length === 0) return { success: false, error: '群不存在: ' + groupId, members: [] };
|
||
|
||
var memberList = (rows[0].memberlist || '').split(';').filter(function (m) { return m; });
|
||
var displayNames = (rows[0].displayname || '').split(';');
|
||
|
||
var members = memberList.map(function (wxid, idx) {
|
||
return {
|
||
wxid: wxid,
|
||
display_name: (displayNames[idx] || '').trim(),
|
||
};
|
||
});
|
||
|
||
return { success: true, group_id: groupId, members: members, count: members.length };
|
||
}
|
||
|
||
function _getGroupInfoFromDB(groupId) {
|
||
var sql = "SELECT chatroomname, memberlist, displayname, roomowner, chatroomnick FROM chatroom"
|
||
+ " WHERE chatroomname='" + groupId.replace(/'/g, "''") + "'";
|
||
var rows = _execSQL(sql);
|
||
if (rows.length === 0) return { success: false, error: '群不存在: ' + groupId };
|
||
|
||
var r = rows[0];
|
||
var memberList = (r.memberlist || '').split(';').filter(function (m) { return m; });
|
||
return {
|
||
success: true,
|
||
group: {
|
||
group_id: r.chatroomname,
|
||
name: r.chatroomnick || '',
|
||
owner: r.roomowner || '',
|
||
member_count: memberList.length,
|
||
members: memberList,
|
||
},
|
||
};
|
||
}
|
||
|
||
function _createGroupInternal(memberIds, topic) {
|
||
return _intentAction(
|
||
'com.workphone.CREATE_GROUP',
|
||
{ member_ids: memberIds.join(','), topic: topic || '' },
|
||
'group_created',
|
||
{ member_ids: memberIds, topic: topic }
|
||
);
|
||
}
|
||
|
||
function _inviteToGroupInternal(groupId, memberIds) {
|
||
return _intentAction(
|
||
'com.workphone.INVITE_GROUP',
|
||
{ group_id: groupId, member_ids: memberIds.join(',') },
|
||
'group_invite',
|
||
{ group_id: groupId, member_ids: memberIds }
|
||
);
|
||
}
|
||
|
||
function _removeFromGroupInternal(groupId, memberIds) {
|
||
return _intentAction(
|
||
'com.workphone.REMOVE_GROUP_MEMBER',
|
||
{ group_id: groupId, member_ids: memberIds.join(',') },
|
||
'group_member_removed',
|
||
{ group_id: groupId, member_ids: memberIds }
|
||
);
|
||
}
|
||
|
||
function _setGroupAnnouncementInternal(groupId, announcement) {
|
||
return _intentAction(
|
||
'com.workphone.SET_GROUP_ANNOUNCEMENT',
|
||
{ group_id: groupId, announcement: announcement }
|
||
);
|
||
}
|
||
|
||
function _setGroupNameInternal(groupId, name) {
|
||
return _intentAction('com.workphone.SET_GROUP_NAME', { group_id: groupId, name: name });
|
||
}
|
||
|
||
function _quitGroupInternal(groupId) {
|
||
return _intentAction(
|
||
'com.workphone.QUIT_GROUP',
|
||
{ group_id: groupId },
|
||
'group_quit',
|
||
{ group_id: groupId }
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// § 9 内部实现 — 朋友圈 (H20/H21)
|
||
// ============================================================
|
||
|
||
// 取当前栈顶 Activity(ActivityThread.mActivities 遍历 paused=false)
|
||
function _getTopActivity() {
|
||
var top = null;
|
||
var lastAny = null; // 兜底:最后一个可取到的 activity(即便 paused)
|
||
var preferred = null; // 优先:分享/选聊天/搜索类 activity(按类名匹配)
|
||
try {
|
||
var ActivityThread = Java.use('android.app.ActivityThread');
|
||
var at = ActivityThread.currentActivityThread();
|
||
var mActivities = at.mActivities.value; // ArrayMap<IBinder, ActivityClientRecord>
|
||
var vals = mActivities.values().toArray();
|
||
for (var i = 0; i < vals.length; i++) {
|
||
var rec = Java.cast(vals[i], Java.use('android.app.ActivityThread$ActivityClientRecord'));
|
||
try {
|
||
var act = rec.activity.value;
|
||
if (act) {
|
||
lastAny = act;
|
||
var cn = '';
|
||
try { cn = act.getClass().getName(); } catch (_) {}
|
||
// 选聊天/分享/搜索界面优先(MIUI 下分享瞬间主 activity 可能短暂 paused)
|
||
if (cn && (cn.indexOf('ShareImgUI') >= 0 || cn.indexOf('SelectConversationUI') >= 0 ||
|
||
cn.indexOf('ShareToTimeLineUI') >= 0 || cn.indexOf('Search') >= 0 ||
|
||
cn.indexOf('SnsUploadUI') >= 0)) {
|
||
preferred = act;
|
||
}
|
||
}
|
||
if (!rec.paused.value) { top = act; break; }
|
||
} catch (_) {}
|
||
}
|
||
} catch (e) {
|
||
log('warn', 'sns', '_getTopActivity 失败: ' + e);
|
||
}
|
||
// 优先级:非paused栈顶 > 分享/选聊天类 > 任意最后一个(best-effort,修「无栈顶 Activity」)
|
||
return top || preferred || lastAny;
|
||
}
|
||
|
||
// 递归遍历 view 树,找文本匹配且可点击的 View 并 performClick
|
||
function _clickViewByText(root, texts) {
|
||
if (!root) return false;
|
||
try {
|
||
var TextView = Java.use('android.widget.TextView');
|
||
var ViewGroup = Java.use('android.view.ViewGroup');
|
||
// 当前节点是 TextView 且文本命中
|
||
try {
|
||
var asTv = Java.cast(root, TextView);
|
||
var t = asTv.getText();
|
||
var s = t ? t.toString() : '';
|
||
for (var k = 0; k < texts.length; k++) {
|
||
if (s.indexOf(texts[k]) >= 0) {
|
||
var clickTarget = root;
|
||
// 向上找可点击祖先(按钮文字常包在可点击容器内)
|
||
var p = root;
|
||
for (var up = 0; up < 4 && p; up++) {
|
||
if (p.isClickable()) { clickTarget = p; break; }
|
||
p = p.getParent();
|
||
}
|
||
if (clickTarget && clickTarget.performClick) {
|
||
clickTarget.performClick();
|
||
log('info', 'sns', '点击发表按钮: "' + s + '"');
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
// 递归子节点
|
||
try {
|
||
var vg = Java.cast(root, ViewGroup);
|
||
var n = vg.getChildCount();
|
||
for (var c = 0; c < n; c++) {
|
||
if (_clickViewByText(vg.getChildAt(c), texts)) return true;
|
||
}
|
||
} catch (_) {}
|
||
} catch (_) {}
|
||
return false;
|
||
}
|
||
|
||
// 遍历找搜索框 EditText 并用 Frida setText 输入(不经 IME/INJECT_EVENTS,直接触发 TextWatcher 过滤)。
|
||
// 用于微信"选择一个聊天"(ShareImgUI):目标联系人常不在最近聊天可见列表,需搜索过滤后才渲染。
|
||
function _findEditTextAndSetText(root, text) {
|
||
if (!root) return false;
|
||
try {
|
||
var EditText = Java.use('android.widget.EditText');
|
||
var ViewGroup = Java.use('android.view.ViewGroup');
|
||
try {
|
||
var et = Java.cast(root, EditText);
|
||
try { et.requestFocus(); } catch (_) {}
|
||
et.setText.overload('java.lang.CharSequence').call(et, text);
|
||
log('info', 'send', '搜索框输入: ' + text);
|
||
return true;
|
||
} catch (_) {}
|
||
try {
|
||
var vg = Java.cast(root, ViewGroup);
|
||
var n = vg.getChildCount();
|
||
for (var c = 0; c < n; c++) {
|
||
if (_findEditTextAndSetText(vg.getChildAt(c), text)) return true;
|
||
}
|
||
} catch (_) {}
|
||
} catch (_) {}
|
||
return false;
|
||
}
|
||
|
||
// 朋友圈发布界面打开后,自动点「发表」(best-effort,失败由 need_confirm_send 兜底)
|
||
function _autoClickPublish(delayMs) {
|
||
try {
|
||
var Handler = Java.use('android.os.Handler');
|
||
var Looper = Java.use('android.os.Looper');
|
||
var handler = Handler.$new(Looper.getMainLooper());
|
||
var Runnable = Java.use('java.lang.Runnable');
|
||
var R = Java.registerClass({
|
||
name: 'com.workphone.PublishClicker' + Date.now(),
|
||
implements: [Runnable],
|
||
methods: {
|
||
run: function () {
|
||
try {
|
||
var act = _getTopActivity();
|
||
if (!act) { log('warn', 'sns', '自动发表:无栈顶 Activity'); return; }
|
||
var decor = act.getWindow().getDecorView();
|
||
var ok = _clickViewByText(decor, ['发表', '完成', 'Post', 'Send']);
|
||
emitEvent('moments_publish_click', { clicked: ok });
|
||
log('info', 'sns', '自动发表点击结果: ' + ok);
|
||
} catch (e) {
|
||
log('warn', 'sns', '自动发表执行失败: ' + e);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
handler.postDelayed(R.$new(), delayMs || 2000);
|
||
return true;
|
||
} catch (e) {
|
||
log('warn', 'sns', '_autoClickPublish 调度失败: ' + e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 分享后 u2 自动选联系人 + 发送(best-effort):两段延迟点击。
|
||
// 第一段点联系人文本(昵称/备注/"文件传输助手"),第二段点"发送"确认。
|
||
// wxid → 微信"选择一个聊天"列表显示名候选解析。
|
||
// 真机实测:to_id=filehelper 在列表里显示为"文件传输助手",按 wxid 文本点不中 → 必须解析显示名。
|
||
// 顺序:DB 备注(conRemark) > 特殊账号映射 > DB 昵称(nickname) > 原 wxid 兜底。
|
||
function _resolveContactDisplayNames(toId) {
|
||
var cands = [];
|
||
var special = {
|
||
'filehelper': ['文件传输助手', 'File Transfer'],
|
||
'fmessage': ['服务通知'],
|
||
'medianote': ['语音记事本'],
|
||
'weixin': ['微信团队', 'WeChat Team'],
|
||
'floatbottle': ['漂流瓶'],
|
||
'newsapp': ['腾讯新闻'],
|
||
};
|
||
// DB 查备注/昵称(备注优先,最贴近列表显示)
|
||
try {
|
||
Java.performNow(function () {
|
||
var esc = String(toId).replace(/'/g, "''");
|
||
var rows = _execSQL("SELECT nickname, conRemark FROM rcontact WHERE username='" + esc + "' LIMIT 1");
|
||
if (rows && rows.length > 0) {
|
||
if (rows[0].conRemark) cands.push(rows[0].conRemark);
|
||
if (rows[0].nickname && rows[0].nickname !== rows[0].conRemark) cands.push(rows[0].nickname);
|
||
}
|
||
});
|
||
} catch (e) { log('warn', 'send', '解析显示名 DB 查询失败: ' + e); }
|
||
if (special[toId]) cands = cands.concat(special[toId]);
|
||
cands.push(toId); // 微信号/wxid 兜底
|
||
var seen = {}, out = [];
|
||
for (var i = 0; i < cands.length; i++) { var v = cands[i]; if (v && !seen[v]) { seen[v] = 1; out.push(v); } }
|
||
return out;
|
||
}
|
||
|
||
function _autoSelectAndSend(toId, delayMs) {
|
||
try {
|
||
var nameCandidates = _resolveContactDisplayNames(toId);
|
||
log('info', 'send', '选联系人候选显示名: ' + JSON.stringify(nameCandidates));
|
||
var Handler = Java.use('android.os.Handler');
|
||
var Looper = Java.use('android.os.Looper');
|
||
var handler = Handler.$new(Looper.getMainLooper());
|
||
var Runnable = Java.use('java.lang.Runnable');
|
||
var d = delayMs || 2500;
|
||
// 第-1段:点"搜索"伪框进真搜索界面(ShareImgUI 顶部搜索是 TextView 伪框,点击才出 EditText)
|
||
var RC = Java.registerClass({
|
||
name: 'com.workphone.ShareSearchOpener' + Date.now(),
|
||
implements: [Runnable],
|
||
methods: {
|
||
run: function () {
|
||
try {
|
||
var act = _getTopActivity();
|
||
if (!act) return;
|
||
var decor = act.getWindow().getDecorView();
|
||
var ok = _clickViewByText(decor, ['搜索', 'Search']);
|
||
log('info', 'send', '点搜索伪框 = ' + ok);
|
||
} catch (e) { log('warn', 'send', '点搜索框失败: ' + e); }
|
||
}
|
||
}
|
||
});
|
||
handler.postDelayed(RC.$new(), d);
|
||
// 第〇段:搜索框输入显示名过滤(最近聊天列表懒加载,目标常不在可见项)
|
||
var RS = Java.registerClass({
|
||
name: 'com.workphone.ShareSearcher' + Date.now(),
|
||
implements: [Runnable],
|
||
methods: {
|
||
run: function () {
|
||
try {
|
||
var act = _getTopActivity();
|
||
if (!act) return;
|
||
var decor = act.getWindow().getDecorView();
|
||
var kw = nameCandidates[0];
|
||
var typed = _findEditTextAndSetText(decor, kw);
|
||
emitEvent('share_search_input', { keyword: kw, typed: typed });
|
||
log('info', 'send', '分享搜索输入 "' + kw + '" = ' + typed);
|
||
} catch (e) { log('warn', 'send', '搜索输入失败: ' + e); }
|
||
}
|
||
}
|
||
});
|
||
handler.postDelayed(RS.$new(), d + 1200);
|
||
var R1 = Java.registerClass({
|
||
name: 'com.workphone.ContactSelector' + Date.now(),
|
||
implements: [Runnable],
|
||
methods: {
|
||
run: function () {
|
||
try {
|
||
var act = _getTopActivity();
|
||
if (!act) { log('warn', 'send', '选联系人:无栈顶 Activity'); return; }
|
||
var decor = act.getWindow().getDecorView();
|
||
var ok = _clickViewByText(decor, nameCandidates);
|
||
emitEvent('share_contact_click', { to_id: toId, candidates: nameCandidates, clicked: ok });
|
||
log('info', 'send', '分享选联系人点击 ' + JSON.stringify(nameCandidates) + ' = ' + ok);
|
||
} catch (e) { log('warn', 'send', '选联系人失败: ' + e); }
|
||
}
|
||
}
|
||
});
|
||
handler.postDelayed(R1.$new(), d + 3100);
|
||
var R2 = Java.registerClass({
|
||
name: 'com.workphone.ShareSendClicker' + Date.now(),
|
||
implements: [Runnable],
|
||
methods: {
|
||
run: function () {
|
||
try {
|
||
var act = _getTopActivity();
|
||
if (!act) return;
|
||
var decor = act.getWindow().getDecorView();
|
||
var ok = _clickViewByText(decor, ['发送', '發送', 'Send', '发送(1)', '發送(1)']);
|
||
emitEvent('share_send_click', { clicked: ok });
|
||
log('info', 'send', '分享发送点击结果: ' + ok);
|
||
} catch (e) { log('warn', 'send', '发送点击失败: ' + e); }
|
||
}
|
||
}
|
||
});
|
||
handler.postDelayed(R2.$new(), d + 4700);
|
||
return true;
|
||
} catch (e) {
|
||
log('warn', 'send', '_autoSelectAndSend 调度失败: ' + e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 真发布:启动微信朋友圈发布 Activity(SnsUploadUI)并预填内容
|
||
// 替代旧的 com.workphone.POST_MOMENTS 广播(无接收端=占位)。
|
||
// 纯文字朋友圈:Ksnsupload_type=9(社区已知),文本 extra 多 key 兜底。
|
||
// 媒体分享真实现(替代 no_receiver 占位):ACTION_SEND 系统分享拉起微信"选择联系人"界面。
|
||
// 微信进程内 startActivity 给自己:关 StrictMode 允许 file:// uri,避免 FileUriExposedException。
|
||
// 阶段一:真拉起微信分享界面(可观测、非占位);阶段二 u2 自动选 to_id + 发送(_autoSelectAndSend)。
|
||
function _shareMediaToWechat(filePath, mimeType, toId, kind) {
|
||
try {
|
||
// 关 VmPolicy 允许 file:// uri 跨组件传递
|
||
try {
|
||
var StrictMode = Java.use('android.os.StrictMode');
|
||
var VmBuilder = Java.use('android.os.StrictMode$VmPolicy$Builder');
|
||
StrictMode.setVmPolicy(VmBuilder.$new().build());
|
||
} catch (_) {}
|
||
|
||
var Intent = Java.use('android.content.Intent');
|
||
var Uri = Java.use('android.net.Uri');
|
||
var File = Java.use('java.io.File');
|
||
var Parcelable = Java.use('android.os.Parcelable');
|
||
var ctx = getCtx();
|
||
|
||
var f = File.$new(filePath);
|
||
var exists = false;
|
||
try { exists = f.exists(); } catch (_) {}
|
||
if (!exists) {
|
||
return { success: false, error: '文件不存在: ' + filePath, kind: kind };
|
||
}
|
||
|
||
var uri = Uri.fromFile(f);
|
||
function buildIntent(cls) {
|
||
var intent = Intent.$new();
|
||
intent.setAction('android.intent.action.SEND');
|
||
intent.setType(mimeType);
|
||
intent.putExtra('android.intent.extra.STREAM', Java.cast(uri, Parcelable));
|
||
intent.setPackage('com.tencent.mm');
|
||
// 直达微信"选择一个聊天"页(ShareImgUI),跳过 MIUI 系统分享面板那一层
|
||
if (cls) intent.setClassName('com.tencent.mm', cls);
|
||
intent.addFlags(0x10000000); // FLAG_ACTIVITY_NEW_TASK
|
||
intent.addFlags(0x00000001); // FLAG_GRANT_READ_URI_PERMISSION
|
||
return intent;
|
||
}
|
||
|
||
var method = 'action_send';
|
||
var started = false;
|
||
var lastErr = '';
|
||
try {
|
||
ctx.startActivity(buildIntent('com.tencent.mm.ui.tools.ShareImgUI'));
|
||
started = true;
|
||
method = 'action_send_direct';
|
||
} catch (e1) {
|
||
lastErr = String(e1);
|
||
}
|
||
if (!started) {
|
||
ctx.startActivity(buildIntent(null));
|
||
started = true;
|
||
method = 'action_send_systempanel';
|
||
}
|
||
|
||
log('info', 'send', 'ACTION_SEND 分享' + kind + ' 拉起微信(' + method + '): ' + filePath + (lastErr ? ' fallback=' + lastErr : ''));
|
||
emitEvent('media_share_ui_opened', { kind: kind, file: filePath, to_id: toId, mime: mimeType, method: method });
|
||
|
||
// 阶段二:u2 自动选联系人 + 发送(best-effort;失败由 need_manual_select 兜底)
|
||
var scheduled = _autoSelectAndSend(toId, 2500);
|
||
|
||
return {
|
||
success: true,
|
||
method: method,
|
||
kind: kind,
|
||
file: filePath,
|
||
to_id: toId,
|
||
auto_select_scheduled: scheduled,
|
||
need_manual_select: !scheduled,
|
||
note: scheduled
|
||
? '已拉起微信分享界面,2.5s 后自动选联系人发送'
|
||
: '已拉起微信分享界面,待选联系人发送(u2 自动化未就绪)',
|
||
};
|
||
} catch (e) {
|
||
return { success: false, error: '分享' + kind + '失败: ' + String(e), kind: kind };
|
||
}
|
||
}
|
||
|
||
// 文本/链接分享真实现(替代 SEND_LINK no_receiver 占位):ACTION_SEND text/plain 拉起微信"选择联系人"。
|
||
// 与 _shareMediaToWechat 同款 intent,仅把 STREAM 换成 EXTRA_TEXT;链接以"标题+换行+URL"纯文本投递。
|
||
// 待真机验证:设备复连后第一批 A 类验证目标。
|
||
function _shareTextToWechat(text, toId) {
|
||
try {
|
||
var Intent = Java.use('android.content.Intent');
|
||
var ctx = getCtx();
|
||
function buildIntent(cls) {
|
||
var intent = Intent.$new();
|
||
intent.setAction('android.intent.action.SEND');
|
||
intent.setType('text/plain');
|
||
intent.putExtra('android.intent.extra.TEXT', text);
|
||
intent.setPackage('com.tencent.mm');
|
||
// 直达微信"选择一个聊天"页(ShareImgUI),跳过 MIUI 系统分享面板那一层
|
||
if (cls) intent.setClassName('com.tencent.mm', cls);
|
||
intent.addFlags(0x10000000); // FLAG_ACTIVITY_NEW_TASK
|
||
return intent;
|
||
}
|
||
|
||
var method = 'action_send_text';
|
||
var started = false;
|
||
var lastErr = '';
|
||
// 真机实测发现:仅 setPackage 时 MIUI 仍弹系统分享面板(多一层"发送给朋友/收藏")。
|
||
// 优先 setClassName 直达 ShareImgUI(微信"选择联系人转发"页),失败再回退系统面板。
|
||
try {
|
||
ctx.startActivity(buildIntent('com.tencent.mm.ui.tools.ShareImgUI'));
|
||
started = true;
|
||
method = 'action_send_text_direct';
|
||
} catch (e1) {
|
||
lastErr = String(e1);
|
||
}
|
||
if (!started) {
|
||
ctx.startActivity(buildIntent(null));
|
||
started = true;
|
||
method = 'action_send_text_systempanel';
|
||
}
|
||
|
||
log('info', 'send', 'ACTION_SEND 文本/链接 拉起微信(' + method + '): len=' + text.length + (lastErr ? ' fallback=' + lastErr : ''));
|
||
emitEvent('text_share_ui_opened', { to_id: toId, text_len: text.length, method: method });
|
||
|
||
var scheduled = _autoSelectAndSend(toId, 2500);
|
||
return {
|
||
success: true,
|
||
method: method,
|
||
to_id: toId,
|
||
auto_select_scheduled: scheduled,
|
||
need_manual_select: !scheduled,
|
||
note: scheduled
|
||
? '已拉起微信分享界面,2.5s 后自动选联系人发送'
|
||
: '已拉起微信分享界面,待选联系人发送(u2 自动化未就绪)',
|
||
};
|
||
} catch (e) {
|
||
return { success: false, error: '文本/链接分享失败: ' + String(e) };
|
||
}
|
||
}
|
||
|
||
function _postMomentsInternal(content, imageUrls, autoSend) {
|
||
imageUrls = imageUrls || [];
|
||
if (autoSend === undefined) autoSend = true;
|
||
var candidates = getCompat('sns_upload'); // [{cls:'...SnsUploadUI'}, ...]
|
||
var lastErr = '';
|
||
for (var i = 0; i < candidates.length; i++) {
|
||
var clsName = candidates[i].cls;
|
||
try {
|
||
var Intent = Java.use('android.content.Intent');
|
||
var ComponentName = Java.use('android.content.ComponentName');
|
||
var ctx = getCtx();
|
||
var SnsCls = Java.use(clsName); // 校验类存在
|
||
var intent = Intent.$new();
|
||
intent.setComponent(ComponentName.$new('com.tencent.mm', clsName));
|
||
intent.addFlags(0x10000000); // FLAG_ACTIVITY_NEW_TASK
|
||
// 纯文字:type=9;图文:type=1(含 imglist)
|
||
var isText = imageUrls.length === 0;
|
||
intent.putExtra('Ksnsupload_type', isText ? 9 : 1);
|
||
// 文本 extra 多 key 兜底(不同版本 key 名不同)
|
||
intent.putExtra('Ksnsupload_text', content);
|
||
intent.putExtra('sns_text', content);
|
||
intent.putExtra('Ksns_upload_text', content);
|
||
if (!isText) {
|
||
intent.putExtra('Ksnsupload_imglist', imageUrls.join(','));
|
||
}
|
||
ctx.startActivity(intent);
|
||
log('info', 'sns', '启动 SnsUploadUI(' + clsName + ') content_len=' + content.length + ' images=' + imageUrls.length);
|
||
emitEvent('moments_post_ui_opened', { content: content, image_count: imageUrls.length, cls: clsName });
|
||
// 打开发布界面+预填后,best-effort 自动点「发表」(失败由 need_confirm_send 兜底)
|
||
var scheduled = autoSend ? _autoClickPublish(2200) : false;
|
||
return {
|
||
success: true,
|
||
method: 'sns_upload_ui',
|
||
cls: clsName,
|
||
auto_click_scheduled: scheduled,
|
||
need_confirm_send: !scheduled,
|
||
note: scheduled ? '已打开朋友圈发布界面并预填,2.2s 后自动点发表' : '已打开发布界面并预填,待点发表',
|
||
sns_id: generateId('sns'),
|
||
};
|
||
} catch (e) {
|
||
lastErr = String(e);
|
||
}
|
||
}
|
||
return { success: false, error: '发朋友圈失败(所有 SnsUploadUI 候选): ' + lastErr };
|
||
}
|
||
|
||
function _getMomentsFromDB(wxid, limit) {
|
||
var snsPath = _getSnsDbPath();
|
||
if (!snsPath) {
|
||
return { success: false, error: 'SnsMicroMsg.db 未找到', moments: [] };
|
||
}
|
||
|
||
var where = wxid
|
||
? " WHERE userName='" + wxid.replace(/'/g, "''") + "'"
|
||
: '';
|
||
// 8.0.69 兼容:探测 SnsInfo 实际列,只 SELECT 存在的列(避免 mediaCount/likeCount/commentCount 缺列致整查询返 0)
|
||
var _wantSns = ["snsId", "userName", "createTime", "content", "mediaCount", "likeCount", "commentCount", "type", "sourceType"];
|
||
var _availSns = {};
|
||
var _snsDiag = {};
|
||
try {
|
||
var _sti = _execSnsSQL("PRAGMA table_info(SnsInfo)");
|
||
(_sti || []).forEach(function (c) { if (c && c.name) _availSns[c.name] = true; });
|
||
_snsDiag.avail_cols = Object.keys(_availSns);
|
||
} catch (eps) { _snsDiag.pragma_err = String(eps); }
|
||
var _useSns = Object.keys(_availSns).length > 0
|
||
? _wantSns.filter(function (c) { return _availSns[c]; })
|
||
: _wantSns;
|
||
if (_useSns.indexOf("snsId") < 0) _useSns.unshift("snsId");
|
||
_snsDiag.use_cols = _useSns;
|
||
var sql = "SELECT " + _useSns.join(", ") + " FROM SnsInfo"
|
||
+ where + " ORDER BY createTime DESC LIMIT " + limit;
|
||
|
||
var rows = _execSnsSQL(sql);
|
||
_snsDiag.rows = (rows || []).length;
|
||
if (!rows || rows.length === 0) {
|
||
try { var sc = _execSnsSQL("SELECT count(*) AS n FROM SnsInfo"); _snsDiag.snsinfo_total = sc && sc[0] ? sc[0].n : null; } catch (e0) { _snsDiag.total_err = String(e0); }
|
||
var hasSnsRows = safeInt(_snsDiag.snsinfo_total, 0) > 0;
|
||
var emptyMoments = _emptyReadFailure('朋友圈', _lastDbDiag, hasSnsRows);
|
||
if (emptyMoments) {
|
||
emptyMoments.moments = [];
|
||
emptyMoments._diag = _snsDiag;
|
||
return emptyMoments;
|
||
}
|
||
}
|
||
return {
|
||
success: true,
|
||
moments: (rows || []).map(function (r) {
|
||
var parsed = _parseSnsContent(r.content);
|
||
return {
|
||
sns_id: r.snsId,
|
||
user_id: r.userName,
|
||
text: parsed.text,
|
||
media_count: safeInt(r.mediaCount, 0),
|
||
like_count: safeInt(r.likeCount, 0),
|
||
comment_count: safeInt(r.commentCount, 0),
|
||
timestamp: r.createTime,
|
||
};
|
||
}),
|
||
count: (rows || []).length,
|
||
_diag: _snsDiag,
|
||
};
|
||
}
|
||
|
||
function _parseSnsContent(raw) {
|
||
if (!raw) return { text: '' };
|
||
try {
|
||
var idx = raw.indexOf('<contentDesc>');
|
||
var endIdx = raw.indexOf('</contentDesc>');
|
||
if (idx >= 0 && endIdx > idx) {
|
||
return { text: raw.substring(idx + 13, endIdx) };
|
||
}
|
||
} catch (_) {}
|
||
return { text: safeStr(raw).substring(0, 200) };
|
||
}
|
||
|
||
function _likeMomentsInternal(snsId) {
|
||
return _intentAction(
|
||
'com.workphone.LIKE_MOMENTS',
|
||
{ sns_id: snsId },
|
||
'moments_liked',
|
||
{ sns_id: snsId }
|
||
);
|
||
}
|
||
|
||
function _commentMomentsInternal(snsId, comment) {
|
||
return _intentAction(
|
||
'com.workphone.COMMENT_MOMENTS',
|
||
{ sns_id: snsId, comment: comment },
|
||
'moments_commented',
|
||
{ sns_id: snsId, comment: comment }
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// § 10 通用 Intent 调度器(所有非 DB 操作统一走此)
|
||
// ============================================================
|
||
|
||
function _intentAction(intentAction, extras, eventType, eventPayload) {
|
||
try {
|
||
return Java.performNow(function () {
|
||
var Intent = Java.use('android.content.Intent');
|
||
var ctx = getCtx();
|
||
var intent = Intent.$new(intentAction);
|
||
var keys = Object.keys(extras || {});
|
||
for (var i = 0; i < keys.length; i++) {
|
||
intent.putExtra(keys[i], String(extras[keys[i]]));
|
||
}
|
||
// 真机铁律 #3/#14:先校验是否存在接收端,无接收端不得返回 success:true(禁止假成功)
|
||
var receivers = 0;
|
||
try {
|
||
var pm = ctx.getPackageManager();
|
||
var list = pm.queryBroadcastReceivers(intent, 0);
|
||
receivers = list ? list.size() : 0;
|
||
} catch (qe) {
|
||
receivers = -1; // 查询失败:未知,按未验证处理
|
||
}
|
||
if (receivers <= 0) {
|
||
var receiverError = receivers === 0 ? 'no_receiver_registered' : 'receiver_query_failed';
|
||
log('warn', 'intent', intentAction + ' 接收端不可验证 → 不广播(避免假成功,需真实 RPC 或装接收端 App)');
|
||
return {
|
||
success: false,
|
||
method: 'intent_broadcast',
|
||
verified: false,
|
||
error: receiverError,
|
||
receivers: receivers,
|
||
action: intentAction,
|
||
hint: '该动作经 Android 广播分发,当前设备未注册接收端;需实现真实 Frida RPC 或部署接收端 App',
|
||
};
|
||
}
|
||
ctx.sendBroadcast(intent);
|
||
if (eventType) {
|
||
emitEvent(eventType, eventPayload || {});
|
||
}
|
||
log('info', 'intent', intentAction + ' → ' + keys.join(',') + ' (receivers=' + receivers + ')');
|
||
return {
|
||
success: true,
|
||
method: 'intent_broadcast',
|
||
verified: receivers > 0,
|
||
receivers: receivers,
|
||
action: intentAction,
|
||
};
|
||
});
|
||
} catch (e) {
|
||
return { success: false, error: String(e), action: intentAction };
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// § 11 Hook 初始化
|
||
// ============================================================
|
||
|
||
Java.perform(function () {
|
||
log('info', 'init', 'wechat_hook_v3.1 正在初始化(110 actions / 24 modules / 版本自适应)...');
|
||
|
||
// 获取微信版本 + 初始化版本兼容
|
||
try {
|
||
var ctx = getCtx();
|
||
var pm = ctx.getPackageManager();
|
||
var info = pm.getPackageInfo(jStr('com.tencent.mm'), 0);
|
||
_wechatVersion = safeStr(info.versionName.value);
|
||
log('info', 'init', '微信版本: ' + _wechatVersion);
|
||
_resolveVersionCompat(_wechatVersion);
|
||
} catch (_) {
|
||
_versionCompat = _DEFAULT_COMPAT;
|
||
log('warn', 'init', '无法获取微信版本,使用 default 全候选');
|
||
}
|
||
|
||
// 查找数据库路径
|
||
_dbPath = _findDbPath(CONFIG.DB_NAME);
|
||
_snsDbPath = _findDbPath(CONFIG.SNS_DB_NAME);
|
||
log('info', 'init', 'EnMicroMsg.db: ' + (_dbPath || '未找到'));
|
||
log('info', 'init', 'SnsMicroMsg.db: ' + (_snsDbPath || '未找到'));
|
||
|
||
// ---- Hook 1: SQLite insert 拦截新消息 (H15) ----
|
||
if (CONFIG.HOOK_SQLITE) {
|
||
try {
|
||
var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase');
|
||
SQLiteDatabase.insert.overload(
|
||
'java.lang.String', 'java.lang.String', 'android.content.ContentValues'
|
||
).implementation = function (table, nullColumnHack, values) {
|
||
var result = this.insert(table, nullColumnHack, values);
|
||
try {
|
||
var tbl = safeStr(table);
|
||
if (tbl === 'message') {
|
||
var talker = safeStr(values.getAsString(jStr('talker')));
|
||
var content = safeStr(values.getAsString(jStr('content')));
|
||
var msgType = safeStr(values.getAsString(jStr('type')));
|
||
var msgSvrId = safeStr(values.getAsString(jStr('msgSvrId')));
|
||
var isSend = safeStr(values.getAsString(jStr('isSend')));
|
||
|
||
if (talker && content) {
|
||
var msg = {
|
||
from_id: talker, content: content, msg_type: msgType,
|
||
msg_svr_id: msgSvrId, is_send: isSend === '1',
|
||
timestamp: new Date().toISOString(),
|
||
};
|
||
_messageBuffer.push(msg);
|
||
if (_messageBuffer.length > CONFIG.MESSAGE_BUFFER_MAX) {
|
||
_messageBuffer = _messageBuffer.slice(-Math.floor(CONFIG.MESSAGE_BUFFER_MAX * 0.6));
|
||
}
|
||
emitEvent('message_received', msg);
|
||
}
|
||
|
||
// H18: 好友请求 type=10002
|
||
if (msgType === '10002') {
|
||
var reqInfo = _parseFriendRequestXml(content);
|
||
var req = {
|
||
msg_id: msgSvrId, from_id: reqInfo.fromusername || talker,
|
||
from_nickname: reqInfo.fromnickname || '',
|
||
encrypt_username: reqInfo.encryptusername || '',
|
||
ticket: reqInfo.ticket || '', content: reqInfo.content || '',
|
||
timestamp: new Date().toISOString(),
|
||
};
|
||
_friendRequestBuffer.push(req);
|
||
if (_friendRequestBuffer.length > CONFIG.FRIEND_REQUEST_BUFFER_MAX) {
|
||
_friendRequestBuffer = _friendRequestBuffer.slice(-60);
|
||
}
|
||
emitEvent('friend_request_received', req);
|
||
log('info', 'hook', '检测到好友请求: ' + req.from_id);
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
return result;
|
||
};
|
||
_hooked.sqlite_insert = true;
|
||
log('info', 'hook', 'SQLite insert 消息拦截已启用');
|
||
} catch (e) {
|
||
log('warn', 'hook', 'SQLite insert Hook 失败: ' + e);
|
||
}
|
||
}
|
||
|
||
// ---- Hook 2: SQLite update 拦截消息撤回 ----
|
||
if (CONFIG.HOOK_SQLITE) {
|
||
try {
|
||
var SQLiteDB2 = Java.use('android.database.sqlite.SQLiteDatabase');
|
||
SQLiteDB2.update.overload(
|
||
'java.lang.String', 'android.content.ContentValues', 'java.lang.String', '[Ljava.lang.String;'
|
||
).implementation = function (table, values, whereClause, whereArgs) {
|
||
try {
|
||
if (safeStr(table) === 'message' && whereClause) {
|
||
var newType = values.getAsInteger(jStr('type'));
|
||
if (newType && newType.intValue() === 10000) {
|
||
var content = safeStr(values.getAsString(jStr('content')));
|
||
if (content.indexOf('撤回') !== -1 || content.indexOf('recalled') !== -1) {
|
||
emitEvent('message_recalled', { where: safeStr(whereClause), content: content });
|
||
log('info', 'hook', '检测到消息撤回');
|
||
}
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
return this.update(table, values, whereClause, whereArgs);
|
||
};
|
||
_hooked.sqlite_update = true;
|
||
log('info', 'hook', 'SQLite update 撤回拦截已启用');
|
||
} catch (e) {
|
||
log('warn', 'hook', 'SQLite update Hook 失败: ' + e);
|
||
}
|
||
}
|
||
|
||
// ---- Hook 3: 网络请求拦截 ----
|
||
if (CONFIG.HOOK_NETWORK) {
|
||
try {
|
||
var URL = Java.use('java.net.URL');
|
||
URL.openConnection.overload().implementation = function () {
|
||
var conn = this.openConnection();
|
||
var url = safeStr(this.toString());
|
||
if (url.indexOf('weixin.qq.com') !== -1 || url.indexOf('wechat') !== -1 || url.indexOf('wx.qq.com') !== -1) {
|
||
emitEvent('network_request', { url: url, method: 'GET' });
|
||
}
|
||
return conn;
|
||
};
|
||
_hooked.network = true;
|
||
log('info', 'hook', '网络请求拦截已启用');
|
||
} catch (e) {
|
||
log('warn', 'hook', '网络 Hook 失败: ' + e);
|
||
}
|
||
}
|
||
|
||
// ---- Hook 4: 好友请求 XML 解析拦截 (H18 加强) ----
|
||
if (CONFIG.HOOK_FRIEND_REQUEST) {
|
||
try {
|
||
// 拦截 AddMsg 处理器(微信内部处理新消息的核心类)
|
||
var candidates = [
|
||
'com.tencent.mm.plugin.messenger.foundation.a.b',
|
||
'com.tencent.mm.plugin.messenger.foundation.a.c',
|
||
];
|
||
for (var ci = 0; ci < candidates.length; ci++) {
|
||
try {
|
||
var Cls = Java.use(candidates[ci]);
|
||
if (Cls) {
|
||
_hooked.friend_request = true;
|
||
log('info', 'hook', '好友请求拦截已启用 (via ' + candidates[ci] + ')');
|
||
break;
|
||
}
|
||
} catch (_) { continue; }
|
||
}
|
||
if (!_hooked.friend_request) {
|
||
log('info', 'hook', '好友请求拦截通过 SQLite 拦截实现(type=10002)');
|
||
_hooked.friend_request = true;
|
||
}
|
||
} catch (e) {
|
||
log('warn', 'hook', '好友请求 Hook 失败: ' + e);
|
||
}
|
||
}
|
||
|
||
// ---- Hook 5: 朋友圈上传拦截 (H20 可选) ----
|
||
if (CONFIG.HOOK_MOMENTS) {
|
||
try {
|
||
var snsCandidates = [
|
||
'com.tencent.mm.plugin.sns.ui.SnsUploadUI',
|
||
'com.tencent.mm.plugin.sns.ui.SnsTimeLineUI',
|
||
];
|
||
for (var si = 0; si < snsCandidates.length; si++) {
|
||
try {
|
||
var SnsCls = Java.use(snsCandidates[si]);
|
||
if (SnsCls) {
|
||
_hooked.sns_upload = true;
|
||
log('info', 'hook', '朋友圈监控已启用 (via ' + snsCandidates[si] + ')');
|
||
break;
|
||
}
|
||
} catch (_) { continue; }
|
||
}
|
||
} catch (e) {
|
||
log('warn', 'hook', '朋友圈 Hook 失败: ' + e);
|
||
}
|
||
}
|
||
|
||
log('info', 'init', 'wechat_hook_v3.1 初始化完成 — 110 actions / 全功能就绪', _hooked);
|
||
emitEvent('hook_initialized', {
|
||
version: '3.0.0',
|
||
wechat_version: _wechatVersion,
|
||
hooked: _hooked,
|
||
db_path: _dbPath || '',
|
||
sns_db_path: _snsDbPath || '',
|
||
});
|
||
});
|