欢迎光临
我们一直在努力

闲鱼 PC 端 IM 消息通道完整技术解析:协议逆向、故障定位与修复实战

闲鱼 PC 端 IM 消息通道完整技术解析:协议逆向、故障定位与修复实战

版本:2026-08 关键词:闲鱼 / goofish / WebSocket / IMPaaS / syncPushPackage / mtop / MessagePack 一句话结论:PC 消息主通道仍是 wss-goofish.dingtalk.com;“连上却收不到”的根因通常不是 API 整体更换,而是 同步游标(pts)握手不完整——必须先 getState 再 ackDiff。


目录

  • 问题背景
  • 系统总览
  • 身份与鉴权体系
  • HTTP 层:mtop 与 Token
  • WebSocket 层:钉钉 IMPaaS
  • 完整握手时序(HAR 对照)
  • 收消息协议详解
  • 发消息协议详解
  • 辅助通道:ACCS
  • 消息解密与字段映射
  • 根因分析
  • 修复方案与实现要点
  • 验证结果
  • 抓包方法论
  • 排障手册
  • 附录

  • 1. 问题背景

    1.1 典型故障现象

    基于闲鱼网页协议的机器人,运行一段时间后出现:

    现象是否正常
    Cookie 仍可访问卖家工作台
    login.token 仍能返回 accessToken
    WebSocket 可连接 wss-goofish.dingtalk.com
    心跳 /! 有响应
    买家实时消息收不到
    手动调用发送接口有时仍可发出 部分情况

    这是一种非常误导人的故障形态:连接层健康,同步层失效。

    1.2 错误假设 vs 正确方向

    常见错误假设实际情况(2026-08)
    消息 API 整体下线 否,主通道仍在
    必须改成 ACCS 收消息 否,ACCS 当前主要是心跳旁路
    发送协议完全变了 否,sendByReceiverScope 未变
    Token API 改名了 否,idlemessage.pc.login.token 仍在用
    只要 Cookie 有效就能收消息 否,还依赖 WS 同步握手

    1.3 排查目标

  • 确认 HTTP Token 链路是否变化
  • 确认 WebSocket 地址 / 注册参数是否变化
  • 对照网页端完整握手,找出与机器人实现的差异
  • 用最小改动恢复收发能力

  • 2. 系统总览

    2.1 逻辑分层

    ┌─────────────────────────────────────────────────────────┐
    │ 业务层:AI 回复 / 人工接管 / 自动发货 / 订单轮询 │
    ├─────────────────────────────────────────────────────────┤
    │ 会话层:chat_id、item_id、buyer_id、上下文 SQLite │
    ├─────────────────────────────────────────────────────────┤
    │ IM 协议层:/reg、getState、ackDiff、syncPush、MessageSend │
    ├─────────────────────────────────────────────────────────┤
    │ 传输层:WebSocket (wss-goofish.dingtalk.com) │
    ├─────────────────────────────────────────────────────────┤
    │ 鉴权层:Cookie + mtop sign + IM accessToken │
    └─────────────────────────────────────────────────────────┘

    2.2 端到端数据流

    浏览器登录 Cookie


    POST h5api.m.goofish.com
    mtop.taobao.idlemessage.pc.login.token
    │ accessToken

    WSS wss://wss-goofish.dingtalk.com/

    ├─ /reg 注册上线,拿到 sid
    ├─ listNewestPagination 拉会话列表(可选)
    ├─ getState 拿服务端真实 pts
    ├─ ackDiff 确认同步游标(关键)

    ├─ 收:/s/sync + syncPushPackage
    │ └─ MessagePack 解密
    │ └─ reminderContent / 订单状态

    └─ 发:/r/MessageSend/sendByReceiverScope

    2.3 关键常量一览

    名称值说明
    站点 https://www.goofish.com PC 网页入口
    mtop 域名 https://h5api.m.goofish.com H5 API 网关
    mtop appKey 34839810 签名用
    IM app-key 444e9908a51d1cb236a27862abc769c9 WS 注册与 token body
    WS 主通道 wss://wss-goofish.dingtalk.com/ 聊天收发
    ACCS 旁路 wss://msgacs.m.taobao.com/accs/auth 推送/心跳辅助
    accountSite xianyu mtop 查询参数
    jsv 2.7.2 mtop JS 版本号
    wv im:3,au:3,sy:6 WS 能力版本声明
    dt j 数据类型(JSON)
    unitName PNM 注册返回的单元名

    2.4 代码模块映射

    文件职责
    main.py / XianyuLive WS 生命周期、握手、收发、业务分发
    XianyuApis.py mtop Token / 商品 / 卖家接口
    utils/xianyu_utils.py Cookie 规范化、签名、deviceId、MessagePack 解密
    context_manager.py 会话上下文与消息落库
    XianyuAgent.py 意图识别与 LLM 回复

    3. 身份与鉴权体系

    闲鱼 PC IM 不是单一 Token,而是 三层凭证叠加。

    3.1 第一层:浏览器 Cookie(登录态)

    常见关键字段:

    Cookie作用
    unb 用户数字 ID(老字段)
    havana_lgc2_* 新登录体系,内含 hid
    _m_h5_tk mtop 签名 token,格式 token_expireTs
    _m_h5_tk_enc mtop token 加密伴生字段
    cookie2 / sgcookie 会话相关
    cna 设备/追踪相关
    XSRF-TOKEN 部分登录接口用

    兼容点:

    • 新 Cookie 可能没有 unb
    • 需从 havana_lgc2_* Base64 JSON 中取 hid
    • 内部实现可把 hid 回填为 unb 以兼容老代码

    3.2 第二层:mtop 签名(HTTP API)

    签名算法:

    sign = md5( f"{_m_h5_tk前半段}&{t}&{appKey}&{data}" )

    其中:

    • _m_h5_tk前半段:cookie['_m_h5_tk'].split('_')[0]
    • t:毫秒时间戳字符串
    • appKey:固定 34839810
    • data:POST body 中的 data 字段原始 JSON 字符串

    示例:

    token = session.cookies.get("_m_h5_tk", "").split("_")[0]
    t = str(int(time.time() * 1000))
    data = '{"appKey":"444e9908a51d1cb236a27862abc769c9","deviceId":"…"}'
    sign = md5(f"{token}&{t}&34839810&{data}")

    3.3 第三层:IM accessToken(WebSocket)

    通过:

    mtop.taobao.idlemessage.pc.login.token

    拿到:

    {
    "accessToken": "oauth_k1:…",
    "accessTokenExpiredTime": "86400000",
    "refreshToken": "oauth_k1:…"
    }

    • accessToken:用于 WS /reg 的 headers.token
    • accessTokenExpiredTime:本次抓包为 86400000(24 小时,毫秒)
    • refreshToken:网页端可能用于续期;当前机器人主路径仍是重新 get_token

    3.4 deviceId 规则

    网页与机器人都使用“UUID + 用户 ID”形式:

    46BDFCA8-9193-4C68-8C0A-970470EF3807-2208946894812
    │______________UUID 部分______________│ │__userId__│

    生成逻辑要点:

  • 前 36 位类似 UUID v4
  • 末尾拼接 -{userId}
  • 同一账号应尽量保持稳定,避免频繁换 did 增加风控特征
  • 3.5 mid / uuid 生成

    字段用途示例格式
    mid 请求-响应关联 ID 3891786173067720 0
    uuid 发送消息去重 ID -17861730736201

    实现参考:

    # mid: 随机前缀 + 毫秒时间戳 + " 0"
    mid = f"{random.randint(0,999)}{int(time.time()*1000)} 0"

    # uuid: 负号 + 毫秒时间戳 + 后缀
    uuid = f"-{int(time.time()*1000)}1"

    服务端响应会带回相同 mid,客户端据此匹配请求结果。


    4. HTTP 层:mtop 与 Token

    4.1 通用 mtop 请求形态

    POST /h5/{api}/{version}/?jsv=2.7.2&appKey=34839810&t=…&sign=…&v=1.0&type=originaljson&accountSite=xianyu&dataType=json&timeout=20000&api={api}&sessionOption=AutoLoginOnly
    Host: h5api.m.goofish.com
    Origin: https://www.goofish.com
    Referer: https://www.goofish.com/
    Content-Type: application/x-www-form-urlencoded

    data={json}

    成功响应通用结构:

    {
    "api": "mtop.xxx",
    "v": "1.0",
    "ret": ["SUCCESS::调用成功"],
    "data": {}
    }

    失败时 ret 可能包含:

    • FAIL_SYS_TOKEN_EXORED / TOKEN_EMPTY:_m_h5_tk 过期
    • SESSION_EXPIRED:登录态失效
    • RGV587_ERROR / USER_VALIDATE:风控/滑块

    4.2 消息页相关 HTTP API 全表

    API版本请求体示例作用
    mtop.taobao.idlemessage.pc.login.token 1.0 {"appKey":"444e…","deviceId":"…"} 拿 IM accessToken
    mtop.taobao.idlemessage.pc.accs.token 1.0 {} 拿 ACCS token
    mtop.taobao.idlemessage.pc.loginuser.get 1.0 {} 当前 IM 用户
    mtop.taobao.idlemessage.pc.redpoint.query 1.0 {"sessionTypes":"1,19,15,…","fetch":50} 未读红点
    mtop.taobao.idlemessage.pc.session.sync 3.0 {"sessionTypes":"[3]","fetchNum":30} 会话同步
    mtop.taobao.idlemessage.pc.user.query 4.0 {"type":0,"sessionType":1,"sessionId":"…","isOwner":false} 用户资料
    mtop.taobao.idlemessage.pc.blacklist.query 1.0 黑名单
    mtop.taobao.idlemessage.face.emoji.load 1.0 表情资源
    mtop.idle.trade.pc.message.headinfo 1.0 {"itemId":…,"sessionId":…,"sessionType":1} 聊天顶栏商品

    4.3 login.token 完整示例

    请求:

    POST /h5/mtop.taobao.idlemessage.pc.login.token/1.0/?jsv=2.7.2&appKey=34839810&t=1786154544791&sign=***&v=1.0&type=originaljson&accountSite=xianyu&dataType=json&timeout=20000&api=mtop.taobao.idlemessage.pc.login.token&sessionOption=AutoLoginOnly&spm_cnt=a21ybx.im.0.0

    Body:

    data={"appKey":"444e9908a51d1cb236a27862abc769c9","deviceId":"46BDFCA8-9193-4C68-8C0A-970470EF3807-2208946894812"}

    Content-Length 约 140,与上述 JSON 长度一致。

    响应:

    {
    "api": "mtop.taobao.idlemessage.pc.login.token",
    "data": {
    "accessToken": "oauth_k1:***",
    "accessTokenExpiredTime": "86400000",
    "refreshToken": "oauth_k1:***"
    },
    "ret": ["SUCCESS::调用成功"],
    "v": "1.0"
    }

    4.4 其他 API 响应要点

    loginuser.get

    {
    "data": {
    "userId": 2208946894812
    }
    }

    redpoint.query

    {
    "req": {"sessionTypes":"1,19,15,32,3,44,51,52,24","fetch":50},
    "data": {"total": 0}
    }

    session.sync v3

    {
    "req": {"sessionTypes":"[3]","fetchNum":30},
    "data": {
    "hasMore": false,
    "sessions": [
    {
    "session": {
    "sessionId": 11402373130,
    "sessionType": 3,
    "ownerInfo": {"userId": "2208946894812", "fishNick": "…"},
    "userInfo": {"userId": "100", "nick": "系统消息", "type": 10}
    },
    "message": {
    "summary": {"summary": "你有50g能量即将过期", "unread": 0}
    }
    }
    ]
    }
    }

    user.query v4

    {
    "req": {
    "type": 0,
    "sessionType": 1,
    "sessionId": "65473344352",
    "isOwner": false
    },
    "data": {
    "needDecryptKeys": ["userInfo.nick"],
    "userInfo": {
    "fishNick": "Elton的小店铺",
    "nick": "x***9",
    "type": 0,
    "logo": "http://img.alicdn.com/…"
    }
    }
    }

    说明:needDecryptKeys 表示部分字段可能做了脱敏/加密展示,网页端有额外解密逻辑;机器人主流程一般不依赖完整昵称明文。

    message.headinfo

    {
    "req": {
    "itemId": 1074426908540,
    "sessionId": 65473344352,
    "sessionType": 1
    },
    "data": {
    "commonData": {
    "itemId": "1074426908540",
    "itemPreInfo": "{\\"soldPrice\\":\\"0.10\\",\\"title\\":\\"…\\",\\"isSelf\\":\\"true\\"}",
    "supportPCTrade": "true"
    },
    "middle": {
    "data": {"price": "0.10", "tips": "广东·深圳"}
    }
    }
    }


    5. WebSocket 层:钉钉 IMPaaS

    5.1 连接参数

    URL: wss://wss-goofish.dingtalk.com/
    Origin: https://www.goofish.com
    UA: Chrome/150…

    网页端 Upgrade 请求头(HAR):

    GET / HTTP/1.1
    Host: wss-goofish.dingtalk.com
    Connection: Upgrade
    Upgrade: websocket
    Origin: https://www.goofish.com
    Sec-WebSocket-Version: 13
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    User-Agent: Mozilla/5.0 … Chrome/150.0.0.0 Safari/537.36

    观察:

    • 浏览器 WS 握手 不一定带 Cookie
    • 鉴权主要靠后续 /reg.headers.token
    • 机器人带 Cookie 一般无害,但不是必须

    5.2 报文总结构

    所有业务帧基本是 JSON:

    {
    "lwp": "/path",
    "headers": {
    "mid": "…",
    "sid": "…",
    "app-key": "…",
    "ua": "…"
    },
    "body": {}
    }

    或响应:

    {
    "code": 200,
    "headers": {"mid": "…", "sid": "…"},
    "body": {}
    }

    5.3 请求-响应关联规则

  • 客户端发送时生成 headers.mid
  • 服务端响应带回相同 mid
  • 服务端主动推送会生成自己的 mid
  • 客户端必须对推送回 ACK:
  • {
    "code": 200,
    "headers": {
    "mid": "<服务端mid>",
    "sid": "<会话sid>",
    "app-key": "…", // 若推送带了就回传
    "ua": "…",
    "dt": "j"
    }
    }

    不 ACK 可能导致推送通道异常或重传堆积。

    5.4 心跳

    客户端:

    {
    "lwp": "/!",
    "headers": {"mid": "4531786173127800 0"}
    }

    服务端:

    {
    "code": 200,
    "headers": {
    "mid": "4531786173127800 0",
    "server-timestamp": "1786173127821"
    }
    }

    建议间隔:15s 左右;超时未响应则重连。


    6. 完整握手时序(HAR 对照)

    以下来自真实 HAR(58 帧)还原,时间顺序已校对。

    6.1 时序图

    Client Server
    | |
    | 1) /reg (token, did, ua, wv) |
    |———————————————->|
    | 2) code=200 + sid/reg-uid/unitName=PNM |
    |<———————————————-|
    | 3) /r/Conversation/listNewestPagination |
    |———————————————->|
    | 4) /s/sync (syncExtraType) |
    |<———————————————-|
    | 5) ACK |
    |———————————————->|
    | 6) /r/SyncStatus/getState |
    |———————————————->|
    | 7) /s/vulcan (历史 syncPushPackage, 可很大) |
    |<———————————————-|
    | 8) ACK |
    |———————————————->|
    | 9) code=200 body={pipeline, pts, timestamp…}|
    |<———————————————-|
    |10) /r/SyncStatus/ackDiff (真实 pts) |
    |———————————————->|
    |11) code=200 |
    |<———————————————-|
    |12) listTop / getByCids / listUserMessages… |
    |———————————————->|
    |13) 进入稳态:心跳 + 实时 /s/sync 推送 |

    6.2 帧 0:/reg

    {
    "lwp": "/reg",
    "headers": {
    "cache-header": "app-key token ua wv",
    "app-key": "444e9908a51d1cb236a27862abc769c9",
    "token": "oauth_k1:***",
    "ua": "Mozilla/5.0 … Chrome/150.0.0.0 Safari/537.36 DingTalk(2.2.0) OS(Windows/10) Browser(Chrome/150.0.0.0) DingWeb/2.2.0 IMPaaS DingWeb/2.2.0",
    "dt": "j",
    "wv": "im:3,au:3,sy:6",
    "sync": "0,0;0;0;",
    "did": "46BDFCA8-9193-4C68-8C0A-970470EF3807-2208946894812",
    "mid": "3891786173067720 0"
    }
    }

    字段解释:

    字段含义
    cache-header 告诉服务端 headers 中关键缓存键
    app-key IM 应用标识
    token login.token 返回的 accessToken
    ua 模拟钉钉 Web IM 容器 UA
    dt=j JSON
    wv 协议能力版本
    sync 初始同步声明
    did 设备 ID

    6.3 帧 1:reg 响应

    {
    "code": 200,
    "headers": {
    "mid": "3891786173067720 0",
    "sid": "2108a6286a76d68b1a13562a109aef8311c030b5c9ac",
    "reg-sid": "2108a6286a76d68b1a13562a109aef8311c030b5c9ac",
    "reg-uid": "2208946894812@goofish",
    "dt": "j",
    "ip-digest": "…",
    "real-ip": "x.x.x.x"
    },
    "body": {
    "unitName": "PNM",
    "cookie": "",
    "timestamp": 1786173067741,
    "isFromChina": true
    }
    }

    实现要点:

    • 保存 sid,后续 ACK 可回传
    • 校验 reg-uid 是否与当前账号一致
    • unitName=PNM 会在 tooLong2Tag 中出现(PNM,1)

    6.4 帧 2:listNewestPagination

    {
    "lwp": "/r/Conversation/listNewestPagination",
    "headers": {"mid": "…"},
    "body": [9007199254740991, 50]
    }

    • 第一个参数类似 cursor(Number.MAX_SAFE_INTEGER)
    • 第二个参数 page size
    • 失败可不阻断收发,但网页会做

    6.5 帧 4:getState

    {
    "lwp": "/r/SyncStatus/getState",
    "headers": {"mid": "9181786173067838 0"},
    "body": [{"topic": "sync"}]
    }

    6.6 帧 8:getState 响应(核心)

    {
    "code": 200,
    "headers": {
    "mid": "9181786173067838 0",
    "sid": "2108a6286a76d68b1a13562a109aef8311c030b5c9ac",
    "dt": "j"
    },
    "body": {
    "pipeline": "sync",
    "tooLong2Tag": "PNM,1",
    "channel": "sync",
    "topic": "sync",
    "highPts": 0,
    "pts": 1786155166159000,
    "seq": 0,
    "timestamp": 1786173067877
    }
    }

    注意:

    • pts 量级通常是“毫秒时间戳再乘 1000”的 16 位左右整数
    • 不能自己瞎编一个当前时间替代
    • 这个对象几乎原样用于下一步 ackDiff

    6.7 帧 9:ackDiff

    {
    "lwp": "/r/SyncStatus/ackDiff",
    "headers": {"mid": "…"},
    "body": [
    {
    "pipeline": "sync",
    "tooLong2Tag": "PNM,1",
    "channel": "sync",
    "topic": "sync",
    "highPts": 0,
    "pts": 1786155166159000,
    "seq": 0,
    "timestamp": 1786173067877
    }
    ]
    }

    这是整次修复中最关键的一步。

    6.8 握手期穿插推送

    在 getState 前后,服务端可能推:

  • /s/sync(同步扩展信息)
  • /s/vulcan(历史批量包,bizType=370 等,payload 很大)
  • 客户端必须:

  • 立即 ACK
  • 不要阻塞等待目标 mid
  • 历史包可暂不进入业务回复逻辑
  • 否则 getState 会被历史包“淹没”导致超时。

    6.9 网页完整 lwp 清单(本次 HAR)

    lwp方向次数级作用
    /reg C→S 1 注册
    /r/Conversation/listNewestPagination C→S 1 最近会话
    /r/SyncStatus/getState C→S 1 同步状态
    /r/SyncStatus/ackDiff C→S 1 确认游标
    /r/Conversation/listTop C→S 1 置顶会话
    /r/Conversation/getByCids C→S 1+ 按 cid 取会话
    /r/MessageManager/listUserMessages C→S 多次 历史消息
    /r/MessageSend/sendByReceiverScope C→S 发送时 发消息
    /r/MessageStatus/read C→S 读后 已读
    /r/Conversation/clearRedPoint C→S 读后 清红点
    /! C→S 周期 心跳
    /s/sync S→C 多次 实时同步
    /s/vulcan S→C 握手期 历史同步

    7. 收消息协议详解

    7.1 实时推送外壳

    {
    "lwp": "/s/sync",
    "headers": {
    "app-key": "444e9908a51d1cb236a27862abc769c9",
    "mid": "ceb9000a 0",
    "sid": "…",
    "ua": "…"
    },
    "body": {
    "syncPushPackage": {
    "maxHighPts": 0,
    "startSeq": 3,
    "endSeq": 3,
    "minCreateTime": 1786173125670,
    "maxPts": 1786173125937000,
    "hasMore": 0,
    "timestamp": 1786173125944,
    "data": [
    {
    "bizType": 40,
    "streamId": "40",
    "objectType": 40000,
    "data": "<base64 payload>"
    }
    ]
    },
    "syncExtensionModel": {
    "reconnectType": 0,
    "failover": 0,
    "fingerprint": 533269192
    }
    }
    }

    字段说明:

    字段含义
    startSeq/endSeq 本次推送序号范围
    maxPts 推送后的最大游标
    hasMore 是否还有后续包
    data[] 业务载荷列表,可能多条
    bizType 业务类型;聊天常见 40
    objectType 对象类型;聊天常见 40000
    data Base64 编码的 MessagePack 或 JSON

    7.2 两类 payload

    A. 纯 JSON Base64(系统/扩展)

    特征:

    • Base64 解码后可直接 json.loads
    • 常见于会话脚本、系统提示、扩展操作
    • 一般不是 C2C 文本聊天
    B. MessagePack Base64(聊天主路径)

    特征:

    • Base64 解码后是二进制 MessagePack
    • 直接当 UTF-8 JSON 会失败
    • 需要 MessagePack 解码成嵌套 map
    • 聊天消息多走这条路径

    7.3 客户端处理流水线

    收到 WS JSON
    → 若有 mid:先 ACK
    → 判断 body.syncPushPackage.data 是否非空
    → 遍历 data[]
    → 尝试 base64+utf8+json
    成功:系统包,跳过
    失败:MessagePack decrypt
    → 判断是否聊天消息 / 订单消息
    → 进入业务层

    7.4 历史消息拉取(非实时)

    点开会话时网页会发:

    {
    "lwp": "/r/MessageManager/listUserMessages",
    "body": [
    "65473344352@goofish",
    false,
    9007199254740991,
    20,
    false
    ]
    }

    参数语义(经验推断):

  • cid
  • 是否正序/某种布尔标志
  • cursor(MAX_SAFE_INTEGER 表示从最新)
  • limit
  • 额外布尔标志
  • 响应中的单条消息模型(节选):

    {
    "message": {
    "messageId": "4241756936958.PNM",
    "cid": "65473344352@goofish",
    "createAt": 1786173125670,
    "sender": {"uid": "2221874527209@goofish"},
    "content": {
    "contentType": 101,
    "custom": {
    "type": 1,
    "data": "eyJhdFVzZXJzIjpbXSwiY29udGVudFR5cGUiOjEsInRleHQiOnsidGV4dCI6IuWkmuWwkemSsSJ9fQ==",
    "summary": "多少钱"
    }
    },
    "extension": {
    "reminderContent": "多少钱",
    "senderUserId": "2221874527209",
    "sessionType": "1",
    "reminderTitle": "Elton的小店铺",
    "reminderUrl": "fleamarket://message_chat?itemId=1074426908540&peerUserId=2221874527209&sid=65473344352&…"
    }
    }
    }

    custom.data 解码后:

    {
    "atUsers": [],
    "contentType": 1,
    "text": {"text": "多少钱"}
    }

    7.5 已读与清红点

    // 已读
    {
    "lwp": "/r/MessageStatus/read",
    "body": [["4241756936958.PNM"]]
    }

    // 清红点
    {
    "lwp": "/r/Conversation/clearRedPoint",
    "body": [[{
    "messageId": "4241756936958.PNM",
    "cid": "65473344352@goofish"
    }]]
    }

    机器人若只做自动回复,可不实现;若要模拟真人已读,可补上。


    8. 发消息协议详解

    8.1 文本消息

    HAR 捕获的真实发送帧(内容 nihaoya):

    {
    "lwp": "/r/MessageSend/sendByReceiverScope",
    "headers": {"mid": "1491786173073621 0"},
    "body": [
    {
    "uuid": "-17861730736201",
    "cid": "65473344352@goofish",
    "conversationType": 1,
    "content": {
    "contentType": 101,
    "custom": {
    "type": 1,
    "data": "eyJjb250ZW50VHlwZSI6MSwidGV4dCI6eyJ0ZXh0IjoibmloYW95YSJ9fQ=="
    }
    },
    "redPointPolicy": 0,
    "extension": {"extJson": "{}"},
    "ctx": {
    "appVersion": "1.0",
    "platform": "web"
    },
    "mtags": {},
    "msgReadStatusSetting": 1
    },
    {
    "actualReceivers": [
    "2221874527209@goofish",
    "2208946894812@goofish"
    ]
    }
    ]
    }

    custom.data 编码前:

    {
    "contentType": 1,
    "text": {"text": "nihaoya"}
    }

    编码方式:

    inner = {"contentType": 1, "text": {"text": text}}
    data_b64 = base64.b64encode(
    json.dumps(inner, ensure_ascii=False).encode("utf-8")
    ).decode("utf-8")

    8.2 字段语义

    字段说明
    cid 会话 ID,格式 {sessionId}@goofish
    conversationType 1 = 单聊
    contentType=101 自定义消息外壳
    custom.type=1 文本类自定义体
    actualReceivers 接收方列表:买家 + 自己
    platform=web 标识网页端来源
    msgReadStatusSetting 已读回执相关

    8.3 图片消息(既有逆向结论)

    图片先上传 CDN,再发 WS:

  • HTTP 上传:https://stream-upload.goofish.com/api/upload.api?appkey=xy_chat
  • WS 发送时 contentType 内层改为 2,并带 image.pics
  • 内层结构概念:

    {
    "atUsers": [],
    "contentType": 2,
    "image": {
    "pics": [{
    "height": 1080,
    "type": 0,
    "url": "https://…",
    "width": 720
    }]
    }
    }

    外层仍走 sendByReceiverScope + contentType=101 + custom.data=base64。

    8.4 发送成功判定

    通常会收到同 mid 的:

    {"code": 200, "headers": {"mid": "…", "sid": "…"}}

    没有 code=200 不代表一定失败(网络延迟),但应做超时监控。


    9. 辅助通道:ACCS

    9.1 获取 ACCS token

    POST mtop.taobao.idlemessage.pc.accs.token
    body: data={}

    响应:

    {
    "data": {"token": "AAAJ…"}
    }

    9.2 连接

    wss://msgacs.m.taobao.com/accs/auth?token=<token>
    Origin: https://www.goofish.com

    9.3 帧内容

    本次 HAR 仅观察到心跳:

    {
    "type": "ACK",
    "protocol": "HEARTBEAT_ACCS_H5",
    "data": ""
    }

    9.4 结论

    问题结论
    ACCS 是否替代钉钉 IM? 目前没有证据
    机器人是否必须接 ACCS? 非必须
    何时再研究 ACCS? 钉钉通道稳定后,若出现仅 ACCS 推业务事件再做

    10. 消息解密与字段映射

    10.1 decrypt 流程

    raw base64 string
    → 清洗非 base64 字符 / 补 padding
    → base64 decode → bytes
    → MessagePack decode → nested map/list
    → JSON 化(数字键保留为字符串键)

    注意:这里的“解密”本质是 解码,不是 AES 一类对称解密。

    10.2 聊天消息识别

    机器人侧常用结构判定:

    def is_chat_message(message: dict) > bool:
    return (
    isinstance(message, dict)
    and isinstance(message.get("1"), dict)
    and isinstance(message["1"].get("10"), dict)
    and "reminderContent" in message["1"]["10"]
    )

    即:

    message["1"]["10"]["reminderContent"] 存在

    10.3 业务字段经验映射

    MessagePack 解码后是“数字键嵌套对象”,不同版本可能有漂移,但常见映射:

    路径(经验)含义
    1 会话/发送方相关结构或 cid
    1.10.reminderContent 消息摘要文本
    1.10.reminderTitle 会话标题/对方昵称
    1.10.reminderUrl deep link,含 itemId/sid/peerUserId
    1.10.senderUserId 发送方 ID
    3.redReminder 订单状态文案(等待发货等)
    extension 中的 itemId 商品 ID

    reminderUrl 示例:

    fleamarket://message_chat?itemId=1074426908540&peerUserId=2221874527209&sid=65473344352&messageId=…

    可直接解析:

    • itemId
    • peerUserId(买家)
    • sid(chat_id / sessionId)

    10.4 订单消息

    当出现:

    redReminder ∈ {等待买家付款, 等待卖家发货, 交易成功, 交易关闭}

    应走订单业务,而不是普通问答。 自动发货通常关注:等待卖家发货。

    10.5 用户 ID 规范

    统一使用:

    纯数字 ID,不带 @goofish 后缀

    协议层多处带后缀:

    2208946894812@goofish
    65473344352@goofish

    业务层存储前应 .split('@')[0]。


    11. 根因分析

    11.1 修复前实现

    get_token()
    connect WS
    send /reg
    sleep(1)
    send ackDiff(pts = now_ms * 1000) ← 本地伪造
    enter loop

    11.2 网页真实实现

    get_token()
    connect WS
    send /reg → wait code 200
    send listNewestPagination
    send getState → wait body.pts
    send ackDiff(真实 pts)
    enter loop

    11.3 差异矩阵

    项目修复前网页/修复后影响
    Token API 正确 正确
    WS URL 正确 正确
    /reg
    UA DingTalk 2.1.5 2.2.0 低~中
    getState
    pts 来源 本地时间 服务端
    ackDiff 有但错误 正确
    发送协议 正确 正确
    解密逻辑 基本正确 增强多 data

    11.4 为什么会“在线却收不到”

    同步系统用 pts 表示“我已经确认到哪里”。

    • 如果客户端上报一个不合理的未来/错误 pts
    • 服务端可能认为“没有 diff 需要推”
    • 或者推送窗口与客户端状态不一致

    表现就是:

    • 连接存活
    • 心跳正常
    • 历史接口/HTTP 正常
    • 实时 /s/sync 业务聊天不来

    这与本次实盘完全吻合。


    12. 修复方案与实现要点

    12.1 目标行为

  • UA 对齐网页
  • /reg 必须等待成功响应
  • 必须 getState 拿到真实 pts
  • ackDiff 使用该 pts
  • 握手期间正确 ACK 插队推送
  • 收包支持多 data[] 与系统包过滤
  • 12.2 关键实现:请求-等待

    async def ws_send_and_wait(ws, payload, timeout=5.0):
    mid = payload["headers"]["mid"]
    await ws.send(json.dumps(payload, ensure_ascii=False))
    deadline = time.time() + timeout
    while time.time() < deadline:
    raw = await asyncio.wait_for(ws.recv(), timeout=deadline time.time())
    data = json.loads(raw)
    resp_mid = (data.get("headers") or {}).get("mid")

    # 命中目标响应
    if resp_mid == mid and "code" in data:
    return data

    # 插队推送:先 ACK,再继续等
    lwp = data.get("lwp") or ""
    if is_sync_package(data) or lwp.startswith("/s/"):
    await ack(ws, data)
    continue
    return None

    12.3 初始化握手

    # 1) reg
    reg_resp = await ws_send_and_wait(ws, {
    "lwp": "/reg",
    "headers": {
    "cache-header": "app-key token ua wv",
    "app-key": IM_APP_KEY,
    "token": access_token,
    "ua": WS_UA_2_2_0,
    "dt": "j",
    "wv": "im:3,au:3,sy:6",
    "sync": "0,0;0;0;",
    "did": device_id,
    "mid": generate_mid(),
    }
    }, timeout=8)

    # 2) 可选:会话列表
    await ws_send_and_wait(ws, {
    "lwp": "/r/Conversation/listNewestPagination",
    "headers": {"mid": generate_mid()},
    "body": [9007199254740991, 50]
    })

    # 3) getState
    state_resp = await ws_send_and_wait(ws, {
    "lwp": "/r/SyncStatus/getState",
    "headers": {"mid": generate_mid()},
    "body": [{"topic": "sync"}]
    })
    sync_state = state_resp["body"] # pipeline/pts/timestamp…

    # 4) ackDiff
    await ws_send_and_wait(ws, {
    "lwp": "/r/SyncStatus/ackDiff",
    "headers": {"mid": generate_mid()},
    "body": [sync_state]
    })

    12.4 降级策略

    若 getState 失败:

  • 记录告警日志
  • 回退本地 pts(兼容旧逻辑)
  • 仍尝试 ackDiff
  • 不直接退出进程(避免 Web 管理端一起挂)
  • 但应把“未拿到真实 pts”视为高优先级健康问题。

    12.5 收包增强

    for item in syncPushPackage["data"]:
    raw = item["data"]
    # 系统 JSON 包
    try:
    json.loads(base64.b64decode(raw).decode("utf-8"))
    continue
    except Exception:
    pass
    # 聊天 MessagePack 包
    msg = json.loads(decrypt(raw))
    if is_chat_message(msg) or "3" in msg:
    handle_business(msg)

    12.6 不建议同时改的部分

    为降低回归风险,本次刻意不动:

    • 发送协议主体
    • Token API 名称与签名算法
    • 业务层 AI 路由
    • ACCS 通道接入

    先恢复“能收”,再谈“增强”。


    13. 验证结果

    13.1 成功日志模板

    WS 注册成功 sid=2132e3d26a771ac6… uid=2208946894812@goofish
    同步游标 getState 成功 pts=1786173128937000
    连接注册完成(reg + getState + ackDiff)
    消息通道已就绪,开始监听买家消息
    用户: Elton的小店铺 (ID: 2221874527209), 商品: 1074426908540, 会话: 65473344352, 消息: Hello
    意图识别完成: default
    机器人回复: 您好亲,全新考研资料,持续更新

    13.2 验证矩阵

    检查项结果
    /reg code=200 通过
    getState 返回 pts 通过
    ackDiff 完成 通过
    实时收到买家消息 通过
    AI 回复发出 通过
    卖家工作台 identity 仍可用 通过

    13.3 结论

    最小闭环已恢复:

    注册 → 同步 → 收消息 → 回复


    14. 抓包方法论

    14.1 工具选择

    工具优点缺点适用
    Chrome net-export Default 易开、全量事件 无 body / 无 WS 帧 初筛域名
    DevTools HAR + content 有 HTTP body、可含 WS 帧 需手动操作 协议修复主路径
    mitmproxy / Fiddler 可控、可脚本化 HTTPS 证书配置 批量分析

    14.2 推荐抓包步骤

  • 登录 https://www.goofish.com
  • 关闭本地机器人,避免会话抢占
  • F12 → Network → Preserve log
  • 进入 /im
  • 切换 2 个会话
  • 买家发 2 条文本,卖家回 1 条
  • Save all as HAR with content
  • 额外复制 wss-goofish.dingtalk.com Messages 关键帧
  • 14.3 必抓帧清单

    • /reg send + response
    • /r/SyncStatus/getState send + response
    • /r/SyncStatus/ackDiff send + response
    • 收到买家消息的 /s/sync
    • 发送消息的 sendByReceiverScope
    • login.token HTTP 请求响应

    14.4 脱敏规范

    公开文档务必处理:

    • Cookie 全量
    • accessToken / refreshToken
    • ACCS token
    • 真实手机号/完整收货地址
    • 可保留:API 名、字段结构、pts 形态、lwp 路径

    15. 排障手册

    15.1 分层定位

    A. Cookie/登录
    B. mtop Token
    C. WS 连接
    D. /reg
    E. getState/ackDiff
    F. 推送到达
    G. 解密/字段识别
    H. 业务过滤(人工接管、过期消息、黑名单)

    15.2 症状 → 原因速查

    症状优先排查
    Token 获取失败 Cookie、_m_h5_tk、风控
    /reg 非 200 accessToken 失效、did/app-key
    有 reg 无 getState 握手代码未更新
    getState 超时 历史推送未 ACK,recv 循环被堵
    全流程成功但仍无消息 解密失败、字段路径变化、业务过滤
    只能收不能发 send body / actualReceivers / cid
    短连接反复断开 风控、多端互踢、token 过期

    15.3 最小复现实验

  • 只跑 WS 客户端,不做 AI
  • 打印所有收到的 lwp 与是否含 syncPushPackage
  • 手机买家号发“ping”
  • 看 10 秒内有无 /s/sync
    • 无 /s/sync:握手/账号/互踢问题
    • 有 /s/sync 无业务:解密/字段问题
    • 有业务无回复:业务层问题

    15.4 日志建议

    至少打印:

    [IM] token ok, expire=…
    [IM] reg ok, sid=…, uid=…
    [IM] getState pts=…
    [IM] ackDiff ok
    [IM] recv lwp=…, syncItems=N
    [IM] chat from=…, cid=…, text=…
    [IM] send ok mid=…


    16. 附录

    附录 A:协议常量速查

    MTOP_HOST=https://h5api.m.goofish.com
    MTOP_APP_KEY=34839810
    IM_APP_KEY=444e9908a51d1cb236a27862abc769c9
    WS_URL=wss://wss-goofish.dingtalk.com/
    ACCS_URL=wss://msgacs.m.taobao.com/accs/auth
    TOKEN_API=mtop.taobao.idlemessage.pc.login.token
    UA_WEB=… Chrome/150 …
    UA_IM=… DingTalk(2.2.0) … DingWeb/2.2.0 IMPaaS DingWeb/2.2.0
    WV=im:3,au:3,sy:6

    附录 B:修复前后对比

    维度修复前修复后
    连接 成功 成功
    注册 成功 成功
    同步游标 本地伪造 服务端真实 pts
    实时收消息 失败/不稳 成功
    发消息 基本可用 可用
    UA 2.1.5 2.2.0
    握手完整性 对齐网页

    附录 C:sessionType 经验值

    sessionType含义(经验)
    1 普通 C2C 聊天
    3 系统/通知类会话
    24 平台助手类(如闲小蜜)
    其他 以 redpoint/session.sync 列表为准

    附录 D:contentType 经验值

    值含义
    1 文本(内层)
    2 图片(内层)
    8 某些会话脚本/系统操作
    101 自定义外壳(外层 WS content)

    附录 E:安全与合规

  • 仅用于自有账号运维与自动化,不用于未授权访问
  • 公开文章必须脱敏 Token/Cookie
  • 控制消息频率,避免触发风控
  • 平台协议可能变更,维护以最新 HAR 为准
  • 附录 F:可继续优化项

  • 接入 refreshToken 续期,减少频繁 login.token
  • 实现已读/清红点,降低“未读堆积”异常
  • 持久化 pts/sid,缩短重连后同步窗口
  • 对 /s/vulcan 历史包做选择性回放,补漏离线消息
  • 监控 fast reconnect 与风控错误码,自动切换等待策略

  • 最终总结

    闲鱼 PC IM 的技术骨架可以概括为:

    Cookie 登录态
    → mtop 签名拿 accessToken
    → 钉钉 IMPaaS WebSocket 注册
    → getState/ackDiff 建立同步游标
    → syncPushPackage 收消息
    → sendByReceiverScope 发消息

    本次故障的核心不是“通道消失”,而是:

    同步握手少了 getState,ackDiff 用了错误 pts,导致实时推送窗口失效。

    把网页端握手补齐后,链路即可恢复:

    注册成功 → pts 同步成功 → 收到 Hello → 机器人正常回复

    这对所有依赖网页 IM 协议的闲鱼自动化项目都有直接复用价值: 先对齐握手,再怀疑业务;先抓 WS 帧,再改 HTTP。


    本文基于 2026-08 对 www.goofish.com/im 的 HAR 对照、协议帧还原与实机验证整理。若后续平台升级协议,请以新抓包为准更新本文中的字段与时序。

    赞(0)
    未经允许不得转载:171主机测评 » 闲鱼 PC 端 IM 消息通道完整技术解析:协议逆向、故障定位与修复实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址