/** * 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'); }