Repository navigation
feat(messages): 发私信 —— xhs send(HTTP short-link,含写入限流) - #84
Open
Jonathan0516 wants to merge 2 commits into
Open
Jonathan0516 wants to merge 2 commits into
Jonathan0516 wants to merge 2 commits into
Conversation
小红书私信不在 `/api/sns/web/` 命名空间,而是独立的 `/api/im/web/` 前缀, 但同在 edith 主机、共用同一套 x-s 签名,所以现有 `_main_api_get` 直接可用, 不需要新的传输层、握手或依赖。 这也补上了一个容易误判的缺口:`/api/sns/web/unread_count`(`xhs unread`) 只统计通知(评论和@ / 赞和收藏 / 新增关注),**不包含私信**,所以此前无法 从 CLI 得知是否有人发来私信。私信未读在 `/api/im/web/chat/get_unread`。 新增: - `MessagingEndpointsMixin` —— `get_im_unread()` / `get_chats()` / `get_chat_messages()` 三个只读端点。 - `xhs chats` —— 会话列表;把 API 埋在 `info` 里的昵称提上来,并按 `--unread`(默认开)合并每个会话的未读数。 - `xhs messages <user_id>` —— 单会话历史,标注 sent/received 方向。 - `unwrap_message_content()` —— 私信正文是多层嵌套 JSON 字符串(卡片/笔记 分享常嵌 3 层以上),逐层剥到最内层文本;卡片优先取 `frontChain` 以保留 "[笔记]" 类型标记,与网页显示一致。畸形输入原样返回,不抛错。 - `tests/test_messaging.py` —— 15 例,纯逻辑,不走网络。 发送私信走的是另一套传输(protobuf over WebSocket,或 RSA 加密的 short-link HTTP 兜底),本 PR 不涉及。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
私信可以不经 IM WebSocket 发送:payload 是 protobuf ChatSendMessage 信封, RSA 加密后 POST 到 /api/im/web/short_link/send_message。该路径与只读端点同在 edith 主机、共用同一套 x-s 签名,因此现有 _main_api_post 直接可用,不需要 握手或长连接。 新增: - im_codec —— 按公开规范实现:protobuf 线格式(protobuf.dev 的 varint / length-delimited)与 RSAES-PKCS1-v1_5(RFC 8017 §7.2.1,交由已声明依赖的 pycryptodome 执行,不手写填充)。无法来自规范的只有服务端公钥和字段号, 二者是线格式的可观测事实,已连同布局记录在模块文档里。 注意本传输不省略默认值字段(空 token/nickname/ref_id 仍占一个 tag, group_chat 显式发 0);proto3 编码器通常会省略,而服务端按字节布局判定, 所以测试用一个逐字节固化的 fixture 钉住布局。 - send_direct_message() —— 解析当前用户 id 作为 sender,编码、加密、POST, 然后要求正面确认:服务端签发的 message_id 才算投递凭证。空响应体、非 dict、无标记的形状一律报 unconfirmed 并带上顶层字段名,便于诊断。宁可 误报失败也不误报成功 —— 写接口的假成功在外联场景里是最坏的失败模式。 返回值同时给出 message_id(服务端)与 local_mid(客户端追踪 id),命名上 不让后者被当成送达证明。 - limiter —— 跨进程写入限流。CLI 每次调用都是新进程,纯内存限流器等于没有, 所以状态落盘在配置目录,滚动窗口令牌桶,默认 1 写/分钟,XHS_WRITE_RPM 可调、设 0 关闭。只作用于 dm bucket,不改变现有写命令的行为。 - xhs send <user_id> -c "...";RateLimitedError 与 rate_limited 错误码接入 既有 envelope。 - resolve_current_user_id() 增加多层回退:不同端点把 id 放在 login_info / basic_info / 顶层,原实现只读顶层,会把已登录用户当成空 id。只增加命中, 不减少,向后兼容。 - tests/test_im_codec.py 与 tests/test_limiter.py 及 send 用例,全部纯逻辑 不走网络。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
在 #83(只读私信)之上叠加发送能力。本 PR 包含 #83 的那一个 commit,因为跨 fork 的 PR 只能以上游
main为 base;review 时看第二个 commitfeat(messages): 发私信即可,#83 合并后这里会自动收敏。传输
私信可以不经 IM WebSocket 发送:payload 是 protobuf
ChatSendMessage信封,RSA 加密后 POST 到/api/im/web/short_link/send_message。这条路径与 #83 的只读端点同在 edith 主机、共用同一套 x-s 签名,所以现有_main_api_post直接可用——不需要握手、长连接或新的传输层。im_codec按公开规范实现,不依赖任何第三方逆向项目的代码:
pycryptodome执行,不手写填充逻辑无法来自规范的只有两样:服务端公钥和消息字段号。二者是线格式的可观测事实而非程序逻辑,已连同布局一并写进模块文档。
有一处反直觉、值得单独说明:这个传输不省略默认值字段。空的
token/nickname/ref_id仍然各占一个 tag + 长度 0,group_chat显式发0。proto3 编码器通常会省略这些,但服务端按字节布局判定,省略会被拒。所以tests/test_im_codec.py用一个逐字节固化的 fixture 把布局钉住——任何改变编码的重构会在这里响,而不是在发送时才炸。不返回假成功
send_direct_message()要求正面确认,而不是"没找到失败标记就算成功":_handle_response对空 body 返回None,所以"缺少失败标记"这种判据会把静默丢弃报成发送成功。写接口的假成功在外联场景里是最坏的失败模式:以为触达了 100 人,实际可能一个都没到。宁可误报失败(可恢复)也不误报成功(不可恢复)。返回值同时给出
message_id(服务端签发,唯一的投递凭证)和local_mid(客户端追踪 id)。命名上把两者分开,避免后者被当成送达证明。写入限流
CLI 每次调用都是一个新进程,纯内存限流器等于没有保护,所以
limiter把滚动窗口令牌桶落盘到配置目录,跨进程共享。默认 1 写/分钟,XHS_WRITE_RPM可调、设0关闭。只作用于新增的
dmbucket,不改变任何现有写命令的行为。同时接入既有错误信封:新增
RateLimitedError与rate_limited错误码(SCHEMA.md 已同步)。一个顺带的修复
resolve_current_user_id()原本只读顶层user_id,但不同端点把 id 放在login_info/basic_info/ 顶层三个位置,所以对已登录用户也可能返回空串。改为逐级回退。只增加命中、不减少,向后兼容。验证
已实测发送成功:同一账号、真实会话,连续两条消息送达(对方随后回复,确认人工可见)。服务端响应形状是:
上面的"正面确认"判据就是按这个真实形状校准的——最初我按
code判断,结果对成功响应误报失败,正好证明了这条判据必须来自实测而不是猜测。codec 重写后的等价性:发送验证用的是等价的早期实现;重写为"从规范实现 + pycryptodome"之后,protobuf 输出与当次成功发送的字节串逐字节一致(即
FIXTURE_HEX),RSA 部分从手写填充改为库实现。需要如实说明:重写后的代码没有再跑一次真实发送,等价性依据是 protobuf 字节相同 + RSA 交由标准库。新增测试全部纯逻辑、不走网络:varint 已知编码、字节布局 fixture、默认值字段必须发出、字段 10 必须不发、UTF-8 长度、RSA 分块对齐与填充随机性、限流的窗口/桶隔离/环境变量/状态文件,以及 send 的端点、限流消耗、未确认形状、非文本 content_type 拒绝、sender 解析失败等。
注意
私信是敏感操作,且平台对私信次数本身有限制。默认 1 写/分钟是有意保守的设置,建议不要为了跑量把它关掉。
🤖 Generated with Claude Code