diff --git a/sdk/agent/hook/wechat_hook_v2.js b/sdk/agent/hook/wechat_hook_v2.js index 026996647e..4db0ce9cd0 100644 --- a/sdk/agent/hook/wechat_hook_v2.js +++ b/sdk/agent/hook/wechat_hook_v2.js @@ -274,16 +274,40 @@ function _getSnsDbPath() { 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(); - if (!path) return results; + _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 + if (typeof db.rawQuery !== 'function') { + _lastDbDiag.error = '实例无 rawQuery(疑 WCDB 2.x core.Database),需 2.x 查询适配'; + log('warn', 'db', _lastDbDiag.error); + return results; + } var cursor = db.rawQuery(sql, null); while (cursor.moveToNext()) { var row = {}; @@ -293,30 +317,88 @@ function _execSQL(sql, dbPathOverride) { 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 实例查找(兼容 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 时按打开处理 +} + function _findOpenWcdb(path) { var normalizedPath = path.replace('/data/data/', '/data/user/0/'); + var baseName = path.split('/').pop(); var found = null; - Java.choose('com.tencent.wcdb.database.SQLiteDatabase', { - onMatch: function(instance) { - if (found) return; - try { - var p = instance.getPath(); - if ((p === path || p === normalizedPath) && instance.isOpen()) { - found = instance; - } - } catch(_) {} - }, - onComplete: function() {} - }); + // 始终遍历全部候选类:不同库分属不同 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; + } + } 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()); } @@ -348,6 +430,21 @@ rpc.exports = { 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) }; + } + }, + getWechatVersion: function () { if (_wechatVersion) return { success: true, version: _wechatVersion }; try { @@ -401,13 +498,34 @@ rpc.exports = { var limit = (params && params.limit) || 50; var conversationId = (params && params.conversation_id) || ''; + var dbRes = null; try { - return Java.performNow(function () { + dbRes = Java.performNow(function () { return _getMessagesFromDB(conversationId, limit); }); } catch (e) { - return { success: false, error: String(e), messages: [] }; + dbRes = { success: false, error: String(e), messages: [] }; } + // DB 读为空时 fallback 到 hook 实时缓冲(insert hook 已拦截新消息) + if (!dbRes || !dbRes.messages || dbRes.messages.length === 0) { + var buf = _messageBuffer.filter(function (m) { + return !conversationId || m.from_id === conversationId; + }).slice(-limit); + 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, + source: 'hook_buffer', + db_diag: _lastDbDiag, + }; + } + if (dbRes) dbRes.db_diag = _lastDbDiag; + } + return dbRes; }, getRecentMessages: function (params) { @@ -683,10 +801,14 @@ rpc.exports = { var content = (params && params.content) || ''; var imageUrls = (params && params.image_urls) || []; if (!content && imageUrls.length === 0) return { success: false, error: '朋友圈内容不能为空' }; - return _intentAction('com.workphone.POST_MOMENTS', { - content: content, - image_urls: (imageUrls || []).join(','), - }, 'moments_posted', { content: content }); + 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) { @@ -935,8 +1057,40 @@ rpc.exports = { return _intentAction('com.workphone.GET_WECHAT_STEPS', {}, null, {}); }, - getStickerList: function () { - return _intentAction('com.workphone.GET_STICKER_LIST', {}, null, {}); + 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) { @@ -1598,9 +1752,25 @@ function _sendMessageInternal(toId, content, msgType) { } } + var messageId = sent ? 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)); + } + } + return { success: sent, - message_id: sent ? generateId('msg') : null, + message_id: messageId, method: sent ? 'hook' : 'failed', to_id: toId, }; @@ -1611,6 +1781,10 @@ function _sendMessageInternal(toId, content, msgType) { // ============================================================ function _getMessagesFromDB(conversationId, limit) { + // DB talker 用 wxid;显示名「文件传输助手」→ filehelper + if (conversationId === '文件传输助手' || conversationId === 'File Transfer') { + conversationId = 'filehelper'; + } var where = conversationId ? " WHERE talker='" + conversationId.replace(/'/g, "''") + "'" : ''; @@ -1631,17 +1805,102 @@ function _getMessagesFromDB(conversationId, limit) { } function _getContactsFromDB(limit) { - var sql = "SELECT username, nickname, conRemark, type FROM rcontact" - + " WHERE type NOT IN (0, 33) AND verifyFlag=0" - + " AND username NOT LIKE '%@chatroom'" - + " AND username NOT LIKE 'gh_%'" - + " ORDER BY nickname LIMIT " + limit; + limit = limit || 200; + 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 sql = _cols + "WHERE type NOT IN (0, 33) AND verifyFlag=0 AND " + _excludeSys + + "ORDER BY conRemark, nickname LIMIT " + limit; var rows = _execSQL(sql); + var _diag = { strict: (rows || []).length, use_cols: _useCols, avail_cols: Object.keys(_availCols) }; + // 8.0.69 兼容回退①:严格过滤(type/verifyFlag)为空时,放宽为仅排除系统/群/公众号 + if (!rows || rows.length === 0) { + var sqlRelax = _cols + "WHERE verifyFlag=0 AND " + _excludeSys + + "ORDER BY conRemark, nickname LIMIT " + limit; + 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 LIMIT " + limit; + 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) { - return { wxid: r.username, nickname: r.nickname, remark: r.conRemark, type: r.type }; + 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 }; + return { success: true, contacts: contacts, count: contacts.length, _diag: _diag }; } function _getContactInfoFromDB(wxid) { @@ -1909,20 +2168,146 @@ function _quitGroupInternal(groupId) { // § 9 内部实现 — 朋友圈 (H20/H21) // ============================================================ -function _postMomentsInternal(content, imageUrls) { +// 取当前栈顶 Activity(ActivityThread.mActivities 遍历 paused=false) +function _getTopActivity() { + var top = null; try { - var Intent = Java.use('android.content.Intent'); - var ctx = getCtx(); - var intent = Intent.$new('com.workphone.POST_MOMENTS'); - intent.putExtra('content', content); - if (imageUrls.length > 0) intent.putExtra('image_urls', imageUrls.join(',')); - ctx.sendBroadcast(intent); - log('info', 'sns', '发布朋友圈: content_len=' + content.length + ' images=' + imageUrls.length); - emitEvent('moments_posted', { content: content, image_count: imageUrls.length }); - return { success: true, method: 'intent_broadcast', sns_id: generateId('sns') }; + var ActivityThread = Java.use('android.app.ActivityThread'); + var at = ActivityThread.currentActivityThread(); + var mActivities = at.mActivities.value; // ArrayMap + 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 { + if (!rec.paused.value) { top = rec.activity.value; break; } + } catch (_) {} + } } catch (e) { - return { success: false, error: '发朋友圈失败: ' + e }; + log('warn', 'sns', '_getTopActivity 失败: ' + e); } + return top; +} + +// 递归遍历 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; +} + +// 朋友圈发布界面打开后,自动点「发表」(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; + } +} + +// 真发布:启动微信朋友圈发布 Activity(SnsUploadUI)并预填内容 +// 替代旧的 com.workphone.POST_MOMENTS 广播(无接收端=占位)。 +// 纯文字朋友圈:Ksnsupload_type=9(社区已知),文本 extra 多 key 兜底。 +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) { @@ -1934,13 +2319,38 @@ function _getMomentsFromDB(wxid, limit) { var where = wxid ? " WHERE userName='" + wxid.replace(/'/g, "''") + "'" : ''; - var sql = "SELECT snsId, userName, createTime, content, mediaCount, likeCount, commentCount FROM SnsInfo" + // 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) { + moments: (rows || []).map(function (r) { var parsed = _parseSnsContent(r.content); return { sns_id: r.snsId, @@ -1952,7 +2362,8 @@ function _getMomentsFromDB(wxid, limit) { timestamp: r.createTime, }; }), - count: rows.length, + count: (rows || []).length, + _diag: _snsDiag, }; } @@ -2011,12 +2422,38 @@ function _intentAction(intentAction, extras, eventType, eventPayload) { 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) { + log('warn', 'intent', intentAction + ' 无接收端 → 不广播(避免假成功,需 _intentAction 真实现或装接收端 App)'); + return { + success: false, + method: 'intent_broadcast', + verified: false, + error: 'no_receiver_registered', + action: intentAction, + hint: '该动作经 Android 广播分发,当前设备未注册接收端;需实现真实 Frida RPC 或部署接收端 App', + }; + } ctx.sendBroadcast(intent); if (eventType) { emitEvent(eventType, eventPayload || {}); } - log('info', 'intent', intentAction + ' → ' + keys.join(',')); - return { success: true, method: 'intent_broadcast', action: intentAction }; + 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 }; diff --git a/sdk/app/agent/hook/wechat_hook_v2.js b/sdk/app/agent/hook/wechat_hook_v2.js index 026996647e..26e5133540 100644 --- a/sdk/app/agent/hook/wechat_hook_v2.js +++ b/sdk/app/agent/hook/wechat_hook_v2.js @@ -935,8 +935,40 @@ rpc.exports = { return _intentAction('com.workphone.GET_WECHAT_STEPS', {}, null, {}); }, - getStickerList: function () { - return _intentAction('com.workphone.GET_STICKER_LIST', {}, null, {}); + 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) { diff --git a/开发文档/8、部署/05-测试验收/20260530_微信全功能真机/get_sticker_list_frida_rpc.json b/开发文档/8、部署/05-测试验收/20260530_微信全功能真机/get_sticker_list_frida_rpc.json new file mode 100644 index 0000000000..d8210e1360 --- /dev/null +++ b/开发文档/8、部署/05-测试验收/20260530_微信全功能真机/get_sticker_list_frida_rpc.json @@ -0,0 +1 @@ +{"code":200,"data":{"success":true,"channel":"hook","columns":["md5","catalog","name","type","size","cdnUrl","groupId","reserved4"],"stickers":[{"md5":"c0059fa4f781a2a500ec03fade10e9b1","name":"icon_002.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"e6f269a19ff2fb61fdb847b39c86ebca","name":"icon_007.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"d629cb3c544fb719405f2b9f16ed6e6c","name":"icon_010.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"e2e2e96798bfbd55b35c3111d89b2e17","name":"icon_012.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"d13e21be9fd71777f727e0c34b0d3994","name":"icon_013.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"68f9864ca5c0a5d823ed7184e113a4aa","name":"icon_018.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"1483ce786912099e29551915e0bc2125","name":"icon_019.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"31574013280aac3897722cc7e3e49ee4","name":"icon_021.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"a00d1de64298d9eaa145ec848a9cc8af","name":"icon_022.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"6257411b26d5aa873762490769625bb9","name":"icon_024.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"5a7fc462d63ef845e6d99c1523bbc91e","name":"icon_027.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"72ebfa527add152c6872219044b151c3","name":"icon_029.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"6a9284bc5ce0bf059375e970a49fa2c5","name":"icon_030.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"6ae79b62bab61132981f1e85ad7070c4","name":"icon_035.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"aab84584b5a3f262286cb38bb107b53e","name":"icon_040.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"bb82ce58f5ed6fdd2b5e34fc2a8e347a","name":"icon_020.gif","catalog":"17","type":"2","size":"0","cdn_url":"0","group_id":"17"},{"md5":"514914788fc461e7205bf0b6ba496c49","name":"jsb_j.png","catalog":"49","type":"1","size":"0","cdn_url":"0","group_id":"49"},{"md5":"f790e342a02e0f99d34b316547f9aeab","name":"jsb_s.png","catalog":"49","type":"1","size":"0","cdn_url":"0","group_id":"49"},{"md5":"091577322c40c05aa3dd701da29d6423","name":"jsb_b.png","catalog":"49","type":"1","size":"0","cdn_url":"0","group_id":"49"},{"md5":"da1c289d4e363f3ce1ff36538903b92f","name":"dice_1.png","catalog":"50","type":"1","size":"0","cdn_url":"0","group_id":"50"}],"count":20,"action_resolved":"get_sticker_list"},"action":"get_sticker_list","channel_used":"websocket/hook"} \ No newline at end of file