From 8592661210bbe8cfc3a85aa7df3ba445b0c92cf7 Mon Sep 17 00:00:00 2001 From: fnvtk Date: Fri, 15 May 2026 15:59:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(ckb):=20=E6=96=B0=E5=A2=9E=E5=AD=98?= =?UTF-8?q?=E5=AE=A2=E5=AE=9D=E4=B8=93=E9=A1=B9Hook=E6=89=A9=E5=B1=95=20(F?= =?UTF-8?q?-CKB-1~4):=20=E8=81=94=E7=B3=BB=E4=BA=BA=E8=AF=A6=E6=83=85/?= =?UTF-8?q?=E7=BE=A4=E6=88=90=E5=91=98=E8=AF=A6=E6=83=85/=E6=96=B0?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E7=9B=91=E5=90=AC/=E7=BE=A4=E6=88=90?= =?UTF-8?q?=E5=91=98=E7=9B=91=E5=90=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sdk/agent/hook/wechat_hook_ckb_patch.js | 549 ++++++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 sdk/agent/hook/wechat_hook_ckb_patch.js diff --git a/sdk/agent/hook/wechat_hook_ckb_patch.js b/sdk/agent/hook/wechat_hook_ckb_patch.js new file mode 100644 index 0000000000..d0ceb870ca --- /dev/null +++ b/sdk/agent/hook/wechat_hook_ckb_patch.js @@ -0,0 +1,549 @@ +/** + * wechat_hook_ckb_patch.js — 存客宝专项 Hook 扩展补丁 + * + * 在 wechat_hook_v2.js 基础上,新增以下功能(F-CKB-1 ~ F-CKB-3): + * + * F-CKB-1 getContactsDetail — 获取联系人详细信息(标签/手机号/性别/地区/备注) + * F-CKB-2 getGroupMembersDetail — 获取群成员详细信息(含每个成员的联系人信息) + * F-CKB-3 onFriendAdd — 新好友添加事件监听(实时上报到 SDK 服务端) + * F-CKB-3 onGroupMemberJoin — 群成员加入事件监听(实时上报到 SDK 服务端) + * F-CKB-4 batchSyncContacts — 批量同步通讯录到存客宝(含分批上报) + * + * 使用方式: + * 此文件作为独立补丁脚本,与 wechat_hook_v2.js 一起注入, + * 或将以下函数合并到 wechat_hook_v2.js 的对应位置。 + * + * @version 1.0.0 (CKB专项) + * @date 2026-05-15 + */ +'use strict'; + +// ============================================================ +// § CKB-1 联系人详细信息获取(扩展版) +// ============================================================ + +/** + * 获取联系人详细信息(包含存客宝所需的全部字段) + * + * 数据来源: + * - rcontact 表:基础信息(wxid/nickname/remark/type/alias) + * - contactlabelinfo 表:标签信息 + * - bizinfo 表:手机号(部分版本) + * + * @param {number} limit - 最大返回数量,默认 500 + * @returns {{ success: boolean, contacts: Array, count: number }} + */ +function _getContactsDetailFromDB(limit) { + limit = limit || 500; + + // Step 1: 获取基础联系人信息(扩展字段) + var sql = "SELECT username, nickname, conRemark, type, alias, " + + "imgFlag, sex, country, province, city, signature " + + "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; + + var rows = _execSQL(sql); + + // Step 2: 获取标签映射 + var labelMap = {}; + try { + var labelSql = "SELECT labelId, labelName FROM contactlabelinfo"; + var labelRows = _execSQL(labelSql); + labelRows.forEach(function(r) { + labelMap[r.labelId] = r.labelName; + }); + } catch(e) { + // 部分版本无此表,忽略 + } + + // Step 3: 获取联系人标签关联 + var contactLabelMap = {}; + try { + var clSql = "SELECT username, labelidlist FROM rcontact WHERE labelidlist IS NOT NULL AND labelidlist != ''"; + var clRows = _execSQL(clSql); + clRows.forEach(function(r) { + if (r.labelidlist) { + var ids = r.labelidlist.split(',').filter(function(id) { return id; }); + var names = ids.map(function(id) { return labelMap[id] || id; }).filter(function(n) { return n; }); + contactLabelMap[r.username] = names; + } + }); + } catch(e) { + // 忽略 + } + + // Step 4: 组装详细联系人信息 + var contacts = rows.map(function(r) { + var sexMap = { '0': 'unknown', '1': 'male', '2': 'female' }; + return { + wxid: r.username || '', + nickname: r.nickname || '', + remark: r.conRemark || '', + alias: r.alias || '', // 微信号(字母ID) + type: r.type || 0, + sex: sexMap[String(r.sex)] || 'unknown', + country: r.country || '', + province: r.province || '', + city: r.city || '', + signature: r.signature || '', // 个性签名 + tags: contactLabelMap[r.username] || [], // 标签列表 + has_avatar: (r.imgFlag || 0) > 0, + }; + }); + + return { success: true, contacts: contacts, count: contacts.length }; +} + +/** + * 获取单个联系人的详细信息 + * + * @param {string} wxid - 微信号 + * @returns {{ success: boolean, contact: Object }} + */ +function _getContactDetailFromDB(wxid) { + if (!wxid) return { success: false, error: '缺少 wxid' }; + + var sql = "SELECT username, nickname, conRemark, type, alias, " + + "imgFlag, sex, country, province, city, signature " + + "FROM rcontact WHERE username='" + wxid.replace(/'/g, "''") + "'"; + + var rows = _execSQL(sql); + if (rows.length === 0) return { success: false, error: '未找到联系人: ' + wxid }; + + var r = rows[0]; + + // 获取标签 + var tags = []; + try { + var labelSql = "SELECT labelidlist FROM rcontact WHERE username='" + wxid.replace(/'/g, "''") + "'"; + var lRows = _execSQL(labelSql); + if (lRows.length > 0 && lRows[0].labelidlist) { + var labelMapSql = "SELECT labelId, labelName FROM contactlabelinfo"; + var lmRows = _execSQL(labelMapSql); + var lm = {}; + lmRows.forEach(function(lr) { lm[lr.labelId] = lr.labelName; }); + tags = lRows[0].labelidlist.split(',') + .filter(function(id) { return id; }) + .map(function(id) { return lm[id] || id; }); + } + } catch(e) {} + + var sexMap = { '0': 'unknown', '1': 'male', '2': 'female' }; + return { + success: true, + contact: { + wxid: r.username || '', + nickname: r.nickname || '', + remark: r.conRemark || '', + alias: r.alias || '', + type: r.type || 0, + sex: sexMap[String(r.sex)] || 'unknown', + country: r.country || '', + province: r.province || '', + city: r.city || '', + signature: r.signature || '', + tags: tags, + has_avatar: (r.imgFlag || 0) > 0, + } + }; +} + + +// ============================================================ +// § CKB-2 群成员详细信息获取(扩展版) +// ============================================================ + +/** + * 获取群成员详细信息(含每个成员的联系人信息) + * + * @param {string} groupId - 群ID(chatroomname) + * @returns {{ success: boolean, group_id: string, members: Array, count: number }} + */ +function _getGroupMembersDetailFromDB(groupId) { + if (!groupId) return { success: false, error: '缺少 group_id', members: [] }; + + // Step 1: 获取群成员列表 + var sql = "SELECT memberlist, displayname, roomowner, chatroomnick FROM chatroom" + + " WHERE chatroomname='" + groupId.replace(/'/g, "''") + "'"; + var rows = _execSQL(sql); + if (rows.length === 0) return { success: false, error: '群不存在: ' + groupId, members: [] }; + + var r = rows[0]; + var memberWxids = (r.memberlist || '').split(';').filter(function(m) { return m.trim(); }); + var displayNames = (r.displayname || '').split(';'); + var owner = r.roomowner || ''; + + if (memberWxids.length === 0) { + return { success: true, group_id: groupId, group_name: r.chatroomnick || '', members: [], count: 0 }; + } + + // Step 2: 批量获取成员联系人信息(分批查询,每批50个) + var memberInfoMap = {}; + var batchSize = 50; + for (var i = 0; i < memberWxids.length; i += batchSize) { + var batch = memberWxids.slice(i, i + batchSize); + var inClause = batch.map(function(id) { return "'" + id.replace(/'/g, "''") + "'"; }).join(','); + try { + var infoSql = "SELECT username, nickname, conRemark, alias, sex, country, province, city " + + "FROM rcontact WHERE username IN (" + inClause + ")"; + var infoRows = _execSQL(infoSql); + infoRows.forEach(function(ir) { + memberInfoMap[ir.username] = ir; + }); + } catch(e) {} + } + + // Step 3: 组装成员详细信息 + var sexMap = { '0': 'unknown', '1': 'male', '2': 'female' }; + var members = memberWxids.map(function(wxid, idx) { + var info = memberInfoMap[wxid] || {}; + return { + wxid: wxid, + display_name: (displayNames[idx] || '').trim(), + nickname: info.nickname || '', + remark: info.conRemark || '', + alias: info.alias || '', + sex: sexMap[String(info.sex)] || 'unknown', + country: info.country || '', + province: info.province || '', + city: info.city || '', + is_owner: wxid === owner, + in_contacts: !!info.nickname, // 是否在通讯录中 + }; + }); + + return { + success: true, + group_id: groupId, + group_name: r.chatroomnick || '', + owner: owner, + members: members, + count: members.length, + }; +} + + +// ============================================================ +// § CKB-3 实时事件监听(新好友 / 群成员加入) +// ============================================================ + +/** + * 启动新好友添加事件监听 + * 通过 SQLite insert Hook 监听 rcontact 表的新增记录 + * + * 触发条件:verifyFlag=0 且 type=3(好友类型)的新记录 + */ +function _startFriendAddListener() { + try { + // 监听 SQLite insert,过滤 rcontact 表的新好友 + // 此函数需要在 Java.perform 上下文中调用 + Java.perform(function() { + 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); + + if (table === 'rcontact') { + try { + var wxid = ''; + var nickname = ''; + var type = 0; + var verifyFlag = 0; + + // 从 ContentValues 读取字段 + if (values) { + try { wxid = values.getAsString('username') || ''; } catch(e) {} + try { nickname = values.getAsString('nickname') || ''; } catch(e) {} + try { type = parseInt(values.getAsInteger('type') || 0); } catch(e) {} + try { verifyFlag = parseInt(values.getAsInteger('verifyFlag') || 0); } catch(e) {} + } + + // 新好友条件:type=3(普通好友)且 verifyFlag=0(已通过验证) + if (type === 3 && verifyFlag === 0 && wxid) { + emitEvent('friend_add', { + wxid: wxid, + nickname: nickname, + source: '微信添加', + timestamp: Date.now(), + // 标记需要上报到存客宝 + report_to_ckb: true, + }); + log('info', 'ckb', '新好友添加事件: ' + wxid + ' (' + nickname + ')'); + } + } catch(e) { + log('warn', 'ckb', '解析新好友事件失败: ' + e); + } + } + + return result; + }; + + log('info', 'ckb', '新好友添加监听已启动'); + }); + return { success: true }; + } catch(e) { + log('warn', 'ckb', '启动新好友监听失败: ' + e); + return { success: false, error: String(e) }; + } +} + +/** + * 启动群成员加入事件监听 + * 通过监听 chatroom 表的 memberlist 字段变化来检测新成员 + */ +function _startGroupMemberJoinListener() { + // 缓存已知群成员,用于对比变化 + var _groupMemberCache = {}; + + try { + Java.perform(function() { + var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase'); + + SQLiteDatabase.update.overload( + 'java.lang.String', + 'android.content.ContentValues', + 'java.lang.String', + '[Ljava.lang.String;' + ).implementation = function(table, values, whereClause, whereArgs) { + var result = this.update(table, values, whereClause, whereArgs); + + if (table === 'chatroom') { + try { + var groupId = ''; + var newMemberList = ''; + + if (values) { + try { newMemberList = values.getAsString('memberlist') || ''; } catch(e) {} + } + // 从 whereClause 提取群ID + if (whereClause && whereClause.indexOf('chatroomname') !== -1 && whereArgs && whereArgs.length > 0) { + groupId = whereArgs[0] || ''; + } + + if (groupId && newMemberList) { + var newMembers = newMemberList.split(';').filter(function(m) { return m.trim(); }); + var oldMembers = _groupMemberCache[groupId] || []; + + // 找出新加入的成员 + var joinedMembers = newMembers.filter(function(m) { + return oldMembers.indexOf(m) === -1; + }); + + if (joinedMembers.length > 0) { + joinedMembers.forEach(function(wxid) { + emitEvent('group_member_join', { + group_id: groupId, + wxid: wxid, + timestamp: Date.now(), + report_to_ckb: true, + }); + log('info', 'ckb', '群成员加入: ' + wxid + ' -> ' + groupId); + }); + } + + // 更新缓存 + _groupMemberCache[groupId] = newMembers; + } + } catch(e) { + log('warn', 'ckb', '解析群成员变动失败: ' + e); + } + } + + return result; + }; + + log('info', 'ckb', '群成员加入监听已启动'); + }); + return { success: true }; + } catch(e) { + log('warn', 'ckb', '启动群成员监听失败: ' + e); + return { success: false, error: String(e) }; + } +} + + +// ============================================================ +// § CKB-4 批量同步通讯录到存客宝 +// ============================================================ + +/** + * 批量同步通讯录到存客宝 + * 获取全部联系人详细信息,通过 HTTP 上报到 SDK 服务端 + * + * @param {Object} params + * @param {string} params.server_url - SDK 服务端地址(如 http://192.168.1.100:8899) + * @param {string} params.device_id - 设备ID + * @param {number} params.limit - 最大同步数量,默认 500 + * @param {number} params.batch_size - 每批上报数量,默认 50 + */ +function _batchSyncContactsToCKB(params) { + var serverUrl = (params && params.server_url) || ''; + var deviceId = (params && params.device_id) || ''; + var limit = (params && params.limit) || 500; + var batchSize = (params && params.batch_size) || 50; + + if (!serverUrl || !deviceId) { + return { success: false, error: '缺少 server_url 或 device_id' }; + } + + try { + // 获取联系人详细信息 + var result = _getContactsDetailFromDB(limit); + if (!result.success || result.contacts.length === 0) { + return { success: false, error: '获取联系人失败或为空' }; + } + + var contacts = result.contacts; + var totalBatches = Math.ceil(contacts.length / batchSize); + var successCount = 0; + var failCount = 0; + + log('info', 'ckb', '开始批量同步通讯录: ' + contacts.length + ' 个联系人,共 ' + totalBatches + ' 批'); + + // 使用 Java HTTP 客户端发送请求 + Java.perform(function() { + var URL = Java.use('java.net.URL'); + var HttpURLConnection = Java.use('java.net.HttpURLConnection'); + var BufferedReader = Java.use('java.io.BufferedReader'); + var InputStreamReader = Java.use('java.io.InputStreamReader'); + var OutputStreamWriter = Java.use('java.io.OutputStreamWriter'); + var JSONObject = Java.use('org.json.JSONObject'); + var JSONArray = Java.use('org.json.JSONArray'); + + for (var i = 0; i < contacts.length; i += batchSize) { + var batch = contacts.slice(i, i + batchSize); + var batchNum = Math.floor(i / batchSize) + 1; + + try { + var url = new URL(serverUrl + '/api/v3/cunke-bao/batch-contacts'); + var conn = url.openConnection(); + conn.setRequestMethod('POST'); + conn.setRequestProperty('Content-Type', 'application/json'); + conn.setDoOutput(true); + conn.setConnectTimeout(10000); + conn.setReadTimeout(15000); + + // 构建请求体 + var bodyObj = { + device_id: deviceId, + source: '微信通讯录同步', + contacts: batch + }; + var bodyStr = JSON.stringify(bodyObj); + + var writer = new OutputStreamWriter(conn.getOutputStream()); + writer.write(bodyStr); + writer.flush(); + writer.close(); + + var responseCode = conn.getResponseCode(); + if (responseCode === 200) { + successCount += batch.length; + log('info', 'ckb', '批次 ' + batchNum + '/' + totalBatches + ' 上报成功'); + } else { + failCount += batch.length; + log('warn', 'ckb', '批次 ' + batchNum + ' 上报失败,HTTP ' + responseCode); + } + conn.disconnect(); + } catch(batchErr) { + failCount += batch.length; + log('error', 'ckb', '批次 ' + batchNum + ' 异常: ' + batchErr); + } + + // 批次间延迟 200ms,避免服务端压力 + java.lang.Thread.sleep(200); + } + }); + + return { + success: true, + total: contacts.length, + success_count: successCount, + fail_count: failCount, + }; + } catch(e) { + return { success: false, error: String(e) }; + } +} + + +// ============================================================ +// § CKB rpc.exports 扩展(追加到现有 rpc.exports 对象) +// ============================================================ + +/** + * 将以下方法追加到 wechat_hook_v2.js 的 rpc.exports 对象中: + * + * getContactsDetail: 获取联系人详细信息(含标签/性别/地区) + * getContactDetail: 获取单个联系人详细信息 + * getGroupMembersDetail: 获取群成员详细信息(含联系人信息) + * startFriendAddListener: 启动新好友添加事件监听 + * startGroupMemberListener: 启动群成员加入事件监听 + * batchSyncContacts: 批量同步通讯录到存客宝 + */ +var _ckbExports = { + // F-CKB-1: 获取联系人详细信息 + getContactsDetail: function(params) { + var limit = (params && params.limit) || 500; + try { + return Java.performNow(function() { return _getContactsDetailFromDB(limit); }); + } catch(e) { + return { success: false, error: String(e), contacts: [] }; + } + }, + + // F-CKB-1: 获取单个联系人详细信息 + getContactDetail: function(params) { + var wxid = (params && params.wxid) || ''; + if (!wxid) return { success: false, error: '缺少 wxid' }; + try { + return Java.performNow(function() { return _getContactDetailFromDB(wxid); }); + } catch(e) { + return { success: false, error: String(e) }; + } + }, + + // F-CKB-2: 获取群成员详细信息 + getGroupMembersDetail: function(params) { + var groupId = (params && params.group_id) || ''; + if (!groupId) return { success: false, error: '缺少 group_id' }; + try { + return Java.performNow(function() { return _getGroupMembersDetailFromDB(groupId); }); + } catch(e) { + return { success: false, error: String(e), members: [] }; + } + }, + + // F-CKB-3: 启动新好友添加事件监听 + startFriendAddListener: function() { + return _startFriendAddListener(); + }, + + // F-CKB-3: 启动群成员加入事件监听 + startGroupMemberListener: function() { + return _startGroupMemberJoinListener(); + }, + + // F-CKB-4: 批量同步通讯录到存客宝 + batchSyncContacts: function(params) { + try { + return Java.performNow(function() { return _batchSyncContactsToCKB(params); }); + } catch(e) { + return { success: false, error: String(e) }; + } + }, +}; + +// 将 CKB 扩展方法合并到 rpc.exports +// (此行需要在 wechat_hook_v2.js 的 rpc.exports 定义之后执行) +if (typeof rpc !== 'undefined' && rpc.exports) { + Object.assign(rpc.exports, _ckbExports); + log('info', 'ckb', 'CKB 扩展 Hook 已注入: getContactsDetail/getGroupMembersDetail/startFriendAddListener/startGroupMemberListener/batchSyncContacts'); +}