Files
workphone-sdk/sdk/agent/hook/wechat_hook_ckb_patch.js

550 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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