背景
做 1688 商家工具,绕不开千牛 IM:自动回复、发商品卡片、发位置消息。但 1688 的聊天不是普通的 HTTP 接口——它走的是 WebSocket + 阿里 LWP 私有协议,网上几乎找不到任何资料。这篇文章记录我从零摸透这套协议、并徒手构造一条位置消息的全过程。
第一步:确定协议栈
打开 1688 聊天页,F12 看 Network,会发现消息收发根本不走 XHR,全部在一条 WebSocket 连接里。所以第一件事是 hook 掉页面的 WebSocket,把真实流量看清楚:
function hookWS() {
if (window.__wsHooked) return console.log('WS 已 hook');
window.__wsHooked = true;
window.__wsList = [];
const Orig = WebSocket;
window.WebSocket = function (...args) {
const ws = new Orig(...args);
window.__wsList.push(ws);
console.log('[WS open]', args[0]);
const origSend = ws.send.bind(ws);
ws.send = function (data) {
const text = typeof data === 'string' ? data : '';
if (!text) return origSend(data);
window.__lastWSSend = text;
const midM = text.match(/"mid"\s*:\s*"?([^",}\s]+)"?/);
if (midM) window.__lastMid = midM[1];
if (/sendByReceiverScope|MessageSend/i.test(text)) {
console.log('[WS send]', text.slice(0, 500));
}
return origSend(data);
};
return ws;
};
window.WebSocket.prototype = Orig.prototype;
}注意一个细节:window.WebSocket.prototype = Orig.prototype 必须保留,否则页面里 instanceof 检查会直接炸。
解剖消息 frame
手动发一条消息,从 hook 日志里就能看到 LWP 协议的真容。一条发送消息的 frame 长这样:
{
"lwp": "/r/MessageSend/sendByReceiverScope",
"headers": { "mid": "1756xxxxxxxxxx" },
"body": [
{
"cid": "2218845998810",
"uuid": "_loc_1756xxxxxxxxxx_1",
"conversationType": 1,
"redPointPolicy": 0,
"extension": {
"senderBizDomain": "cbu",
"receiverBizDomain": "cbu",
"pushMsgType": "bc_chat",
"senderMainUserId": 2218xxxxxxxx,
"sender_nick": "cnalichn你的昵称"
},
"content": {
"contentType": 101,
"custom": {
"type": 10,
"data": "eyJsb2NhdGlvbk5hbWUiOiIuLi4ifQ=="
}
},
"ctx": { "senderBizDomain": "cbu", "receiverBizDomain": "cbu" }
},
{ "actualReceivers": ["cnalichn对方昵称"] }
]
}关键字段逐个拆解:
lwp: /r/MessageSend/sendByReceiverScope:LWP 的路由路径,相当于 HTTP 里的 URLheaders.mid:消息 ID,单调递增,直接复用主程序上一条的 mid 最稳妥cid:会话 IDcontent.contentType: 101:自定义消息类型custom.type: 10+ base64 的{locationName}:位置消息的载体actualReceivers:接收方 nick,注意 nick 都要带cnalichn前缀(1688 的 appKey)
会话参数从哪来
构造消息需要四个参数:cid、pairSecond(对方 nick)、senderMainUserId、自己的 sender_nick。它们散落在页面各处:
- cid:藏在
window._vs里,但路径不固定,我的办法是递归深搜整个对象,找cid字段或以@开头的长字符串 - senderMainUserId:优先从
_vs.loginID里取,取不到就从 cookie 的unb字段兜底 - nick:从任意一条历史消息的
ext.sender_nick/receiver_nick推断,排除掉"官方助手"就是自己和对方
function findCidInVS(vs = window._vs || {}) {
const hits = [];
(function walk(o, path = '_vs', depth = 0, seen = new WeakSet()) {
if (!o || typeof o !== 'object' || depth > 8) return;
if (seen.has(o)) return;
seen.add(o);
for (const [k, v] of Object.entries(o)) {
if (/^cid$/i.test(k) && v) hits.push({ path: path + '.' + k, cid: String(v) });
if (typeof v === 'string' && /^@/.test(v) && v.length > 10) {
hits.push({ path: path + '.' + k, cid: v });
}
if (v && typeof v === 'object') walk(v, path + '.' + k, depth + 1, seen);
}
})(vs);
return hits[0]?.cid || vs.cid || new URLSearchParams(location.search).get('cid') || null;
}递归时一定要带 WeakSet 去重和深度限制,_vs 里有循环引用,裸递归直接栈溢出。
最大的坑:cbu_url_safety
消息内容里只要带链接,直接发出去会被风控拦下。抓包对比发现,主程序发送带链接的消息时,extension 里会多一个 cbu_url_safety 字段,结构还不公开。
解决办法很"流氓"但有效:先 hook 着,手动发一条带链接的消息,把主程序生成的 cbu_url_safety 样本偷下来,之后构造消息时照着样本填:
// hook 时顺手抓样本
const safetyM = text.match(/"cbu_url_safety"\s*:\s*("(?:\\.|[^"\\])*"|{[\s\S]*?})/);
if (safetyM) {
try {
window.__cbuUrlSafetySample = JSON.parse(safetyM[1]);
} catch {
window.__cbuUrlSafetySample = safetyM[1];
}
}
// 构造时复用样本结构,只替换 url
if (Array.isArray(parsed)) {
return JSON.stringify(urls.map((url, i) => ({
...(parsed[i] || parsed[0] || {}), url
})));
}实在没有样本时的兜底结构:
return JSON.stringify({
urlList: urls.map((url) => ({
url,
domain: new URL(url).hostname,
safe: true,
})),
});发送
frame 构造好后,直接往 hook 到的最后一个 WebSocket 里 send 即可:
function sendFrame(frame = window.__frame) {
const ws = window.__wsList?.[window.__wsList.length - 1];
if (!ws || ws.readyState !== WebSocket.OPEN) {
throw new Error('没有可用 WebSocket,请先 hookWS() 并重进聊天页');
}
ws.send(typeof frame === 'string' ? frame : JSON.stringify(frame));
}对方客户端实时收到位置消息卡片,和手动发送的完全一致。
总结
1688 IM 的门槛不在加密,而在协议完全不公开:LWP 路由、frame 结构、nick 前缀、cbu_url_safety,每一步都得靠 hook 主程序"偷样本"来对齐。这套"hook WS → 抓样本 → 构造 frame → 复用连接发送"的方法论,同样适用于钉钉、千牛卖家版等所有阿里系 IM。
完整工具脚本约 350 行,需要交流的微信:teawhites