feat: publish workphone SDK deployment and API docs

This commit is contained in:
Manus AI
2026-07-14 18:10:52 +08:00
commit 021d633cc1
534 changed files with 122391 additions and 0 deletions

View File

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