Skip to content

message send 恒返回 ok:true(不读 ack);register() 的 ackDiff 返回 400 导致消息不落库 #32

Description

@Jonathan0516

现象

message send 对每一次发送都返回 ok: true + mid,但消息实际没有送达。在 v0.4.0 上复现两次(一次发给系统号 session_type=25,一次发给真实 session_type=1 会话),账号方均确认对话里没有出现该消息。

message history 回读同一 cid 也看不到发出的消息(多次重试均如此)。

排查过程中确认不是以下原因:

  • auth status → valid: true
  • 无 ~/.goofish-cli/guard.json,RGV587 熔断未触发
  • limiter.json 正常记录 message.write
  • /reg 鉴权成功(code: 200,reg-uid 正确),服务端持续下推 /s/sync、/s/vulcan

原因一:ok 与 mid 都是本地构造的,不携带投递信息

core/ws.py _send_custom(L171、L197):

mid = generate_mid()      # 本地生成
...
await ws.send(json.dumps(msg))
return mid                # 不读回包

commands/message/send.py L98-L102:

            # 等一轮 ack 回包,避免 WS 提前关
            await asyncio.sleep(1.0)
        finally:
            hb.cancel()
    return {"cid": cid, "toid": toid, "kind": kind, "ok": True, "mid": mid}

注释写的是「等一轮 ack 回包」,但实现只 sleep(1.0),从未读取或校验该 ack;ok: True 为硬编码。_send 内也没有任何 recv 循环,因此服务端返回的任何错误都不可见。

结果:ok 恒为 true,只要 ws.send() 未抛异常。对需要投递确认的上层(例如 #27 讨论的「发送成功 / 发送未知」状态契约)而言,当前返回值无法作为依据。

原因二:register() 的 ackDiff 返回 code: 400,sync 状态未建立

用仓库自身的 connect() + register() 连接并读取回包,收到的帧依次为:

[0] code=400   ← /r/SyncStatus/ackDiff 被拒
[1] code=200   ← /reg 成功(reg-uid 正确)
[2] lwp=/s/sync
[3] lwp=/s/vulcan

sendByReceiverScope 应依赖有效的 sync 状态,ackDiff 被拒可能是消息不落库的直接原因。

对比同仓库内两处 ackDiff 的参数差异:

调用点 pts 结果
core/ws.py L101 register() current_ms * 1000 code: 400
core/ws.py L263 collect_session_cids() 0 正常(list-chats --watch-secs 能发现会话)

collect_session_cids 的 docstring 也明确写的是 ackDiff(pts=0)。

建议修法

把 register() L101 的 "pts": current_ms * 1000 改为 "pts": 0,与 collect_session_cids() 中已验证可用的值保持一致。

旁证:断连频率异常

run_forever docstring 记录「服务端会不定时关连接(观察约每 1030 分钟一次)」。在 sync 未建立的情况下实测为 6 分钟内断连 5 次(间隔 12 分钟,退避 1→2→4→8→16s),比预期频繁约一个数量级。修好 ackDiff 后这一项值得复测。

建议

  1. send_text / send_image 读取并校验 ack 帧,把服务端返回的 code 反映到 ok;无法确认时返回第三态(如 ok: "unknown")而非 true。
  2. register() 校验 /reg 与 ackDiff 的响应码,失败时报错而非静默继续。
  3. 修正 register() 的 pts 参数(见上)。

环境

  • goofish-cli 0.4.0 (PyPI),源码核对 6fb870a
  • macOS 26.6 / Python 3.14.4
  • 登录方式:auth login <cookies.json>(浏览器 cookie 导出导入,非 --qr)

(帖内所有账号 ID、会话 cid、sid、IP 均已去除。)

Activity

  1. Jonathan0516 commented on Sep 10, 2026

    @Jonathan0516
    ContributorAuthor

    更正 + 已定位真正根因(附验证过的修法)

    抱歉,原帖里关于 ackDiff 的两条判断都是错的,先撤回:

    • ❌ 「把 register() 的 pts 改成 0 可修」—— 实测改了,ackDiff 仍返回 400。
    • ❌ 「ackDiff 的 400 可能是消息不落库的直接原因」—— 它和发送失败是同一个根因的两个症状,不是因果。

    原帖第一条结论(ok: True 硬编码、不读 ack)成立,且正是它掩盖了下面这个真正的问题。

    真正根因:/r/ 请求在 /s/vulcan 到达之前发出会被 400 拒绝

    对比同文件内一个能成功的 /r/ 调用和两个失败的:

    core/ws.py list_user_messages()(成功):

    async for raw in ws:
        ...
        with suppress(Exception):
            await ws.send(json.dumps(build_ack(msg)))   # ① 对每个下行帧回 ack
        lwp = msg.get("lwp")
        if lwp == "/s/vulcan":
            await ws.send(json.dumps(req))              # ② 等到 /s/vulcan 才发请求
            continue

    commands/message/send.py _send()(失败):

    await register(ws, session, token)
    hb = asyncio.create_task(heartbeat_loop(ws))
    ...
    mid = await send_text(...)     # 紧跟 register 立即发,既不等 /s/vulcan 也不回 ack

    按 mid 精确配对回包,规律无例外:

    调用点 是否等 /s/vulcan ack code
    list_user_messages()(message history) 是 200
    collect_session_cids() 只监听,不发 /r/ —
    register() 内的 ackDiff 否(紧跟 /reg) 400
    _send() 的 sendByReceiverScope 否 400

    验证过的修法

    在 _send() 里 register() 之后、发送之前插入「等 /s/vulcan 并对下行帧回 build_ack」,其余不变。改完后同一 cid、同一账号:

    {
      "ok": true,
      "ack_code": 200,
      "ack_body": {
        "messageId": "<server-side id>",
        "receiverCount": 2,
        "unreadCount": 1,
        "createAt": <ts>
      }
    }

    message history 回读也确认消息已落库(原本 2 条 → 3 条)。同一台机器、同一账号,改动前后唯一差异就是这个等待。

    建议 register() 里的 ackDiff 也一并移到 /s/vulcan 之后再发,或者由调用方在就绪后自行发送。

    另一个独立问题:session_id 混了两个命名空间

    commands/message/list_chats.py:

    "session_id": str(session.get("sessionId", ""))   # baseline:MTOP sessionId
    "session_id": str(w["cid"])                       # watch:IM cid

    message history / message send 需要的是 IM cid,而默认(不带 --watch-secs)的 list-chats 返回的全是 baseline 的 MTOP sessionId。拿它去调 history 会得到空数组、调 send 会得到 400,且都没有提示。

    建议拆成两个字段(如 session_id / cid),或在 record 里标注该 id 能否用于 history/send。这个也解释了为什么 goofish-reply-buyer/SKILL.md 里写的 “list-chats 返回 cid” 与实际 schema 不一致。

    环境

    goofish-cli 0.4.0 (PyPI),源码核对 6fb870a,macOS 26.6 / Python 3.14.4,auth login <cookies.json> 导入登录态。

    (所有账号 ID、cid、sid、IP、messageId 均已去除或占位。)

  2. Jonathan0516 commented on Sep 10, 2026

    @Jonathan0516
    ContributorAuthor

    再更正一次:session_id 命名空间那条也不成立

    上一条评论里我说「list_chats 的 baseline session_id 是 MTOP sessionId,拿去调 history 会得空数组、调 send 会得 400」——这条是错的,撤回。

    修好 /s/vulcan 等待之后复测:拿 baseline 来源(source: baseline、session_type: 25)的 session_id 发送,成功,服务端签发了 messageId;随后 message history 用同一个 id 也能读回该消息。

    之前 history 对这个 id 返回 [],原因只是那个会话本来没有可检索的用户消息(推送频道里的营销内容不算),不是 id 类型不对。发送成功一条之后,history 就返回 1 条了。

    所以 baseline 和 watch 两个来源的 id 对 history / send 都可用;两者的差别只在会话列表完整度(baseline 只有活跃 Top N,漏掉部分会话),而这一点 list_chats.py 的模块 docstring 已经写清楚了,是设计取舍不是缺陷。

    最终结论:真正的根因只有一个——/r/ 请求早于 /s/vulcan 发出会被 400 拒绝,而 ok: True 硬编码把它完全藏住了。已在 #33 提交修复。

    抱歉来回更正了两次,前两个猜测都没实测到底就发了出来。#33 里的每一条都有对应的实测记录。

  3. fancyboi999 commented on Sep 14, 2026

    @fancyboi999
    Owner

    补充一次当前状态的真实业务验证:

    • auth login --qr:扫码成功,获得完整 session cookies
    • auth status:valid=true
    • message list-chats:真实返回会话列表
    • message history:真实读取目标会话
    • 使用当前 main(旧版发送逻辑)发送文本 hi
    • message send 返回 ok=true
    • 发送后再次调用 message history,成功回读到由当前账号发送的 hi

    因此 Issue #32 的现象在本次账号、会话和环境中未复现;但这不能证明问题不存在。当前代码仍然存在客观缺陷:ok=true 是本地硬编码,未读取或匹配服务端发送 ack,因此服务端拒绝或超时仍可能报告假成功。

    建议暂不把一次成功视为问题已解决,也不建议仅凭单元测试合并修复。请在失败场景补充脱敏后的 /reg、ackDiff、sendByReceiverScope 回包 code/mid 及发送后 history 回读证据,以区分偶发时序问题、服务端拒绝和回读延迟。

  4. fancyboi999 commented on Sep 14, 2026

    @fancyboi999
    Owner

    已通过 PR #33 修复并合入 main(commit 39d70ea)。全量真机 E2E 验证确认发送等待 /s/vulcan、校验服务端 ack 及返回服务端 message_id 均已生效,回读确认落库。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions