fix(lark): bound an answer delivery by the provider, not by the notice length - #4874
Conversation
…e length The 1200-character limit is a compact shape for a chat-root notification, not a provider bound; the provider accepts 150 KB in one text message. Every delivery that carried no source message inherited it as if it were a transport limit, so an ordinary long answer was cut and handed to the bounded part sequence even though one message would have fit. Make the limit an explicit parameter: the notification caller owns it, and the answer deliveries (the manager answer, the manager return and the part fallback) declare the provider bound instead. The part sequence stays as the fallback for a body the provider itself cannot take. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
One route-level case drives a 3000-character steward answer through process_lark_goal_topic_event and asserts exactly one reply carrying the whole body with no part record, and one focused case asserts the same over-limit text is still rejected as a chat-root notification while the answer bound accepts it. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Exact head: 5855bb44b31d1636ccfd161155c04a5d6d91edef (re-read immediately before publishing).
动机
管家行 todo_1b80f3e82483 要求管家通道投递自愈且幂等。这次先做了实测,结论是:切分所依据的 1200 字符不是 provider 的限制,是我们自己的。
loopx/extensions/lark/outbound.py:DEFAULT_LARK_TEXT_LIMIT = 1200,而 provider 的真实上限是LARK_TEXT_REQUEST_MAX_BYTES = 150_000(150 KB,报错文案即 "exceeds the 150 KB provider limit")。- 真机实测(同一 bot、同一个群、同一条链路,
reply_lark_event_inbox带已捕获源消息):782 / 1083 / 1269 / 8244 字符的正文都单条投递成功并读回确认(sent_verified、reply_verified: true,placementchat_root)。 - 路由级验证:3000 字符的管家答复在当前 main 上本来就是 一条消息,不产生分段记录。
根因是把「通知形态的紧凑长度」当成了传输限制:_deliver_lark_inbox_outbound 在没有匹配到源事件时一律套 1200,于是不带源消息的答复投递继承了它,本该一条发完的长答复被切断交给分段序列。代价是每条答复多一条消息、多一份分段记录、多一处读者要自己拼的地方;而分段一旦卡住,读者手里就只剩裸片段——正是 PR #4869 在补的那类事故。
改动思路
把这条长度从「隐式继承」改成「显式归属」:
short_message_limit成为_deliver_lark_inbox_outbound/reply_lark_event_inbox的显式参数,默认仍是DEFAULT_LARK_TEXT_LIMIT,因此没有调用方改变行为。- chat-root 通知(
send_lark_inbox_message)显式声明它拥有这个紧凑上限:它是通道上的一条通知,不是一条答复,读者预期它短。 - 答复投递(管家答复、管家 return、分段兜底)显式传
None,声明只受 provider 的请求上限约束。 - 分段序列保持不变,作为「provider 自己也接不下的正文」或格式校验失败时的兜底,因此 PR #4869 的「分段卡住通知」仍然有意义。
复用的是既有的 normalize_lark_outbound_text 与 validate_lark_text_request_size(真正的 150 KB 字节校验),没有新增第二套长度策略,也没有新增持久化字段。
具体改动
loopx/extensions/lark/inbox_reply.py:新增short_message_limit参数(默认DEFAULT_LARK_TEXT_LIMIT,并把DEFAULT_LARK_TEXT_LIMIT引入 import),在_deliver_lark_inbox_outbound内按None if source_event is not None else short_message_limit应用;send_lark_inbox_message显式传紧凑上限;reply_lark_event_inbox透传。loopx/extensions/lark/goal_topic_runtime.py:管家答复的单条发送传short_message_limit=None。loopx/extensions/lark/manager_returns.py:管家 return 投递传None。loopx/extensions/lark/manager_reply_parts.py:分段兜底发段时传None(段本身已按预算切好)。- 新增测试
test_a_long_manager_answer_is_delivered_as_one_message:驱动完整process_lark_goal_topic_event,3000 字符答复 → 恰好一次+messages-reply、正文完整、落盘记录里没有delivery_part_count。 - 新增测试
test_an_answer_delivery_is_bounded_by_the_provider_not_by_the_notice_length:同一段超限正文在通知路径仍被拒(且不产生任何 provider 调用),在答复路径被接受。
关键代码讲解
1. _deliver_lark_inbox_outbound(inbox_reply.py:272)——长度归属变成参数
short_message_limit: int | None = DEFAULT_LARK_TEXT_LIMIT,
...
reply_text = normalize_lark_outbound_text(
text,
limit=None if source_event is not None else short_message_limit,
preserve_format=markdown,
)带已捕获源消息时仍然是「不套自设上限」(这是既有行为,实测 8244 字符单条通过就是走这条路);不带源消息时才用调用方声明的上限。默认值等于旧常量,所以任何不传该参数的调用方行为逐字不变。
2. send_lark_inbox_message(inbox_reply.py:904)——通知明确拥有紧凑长度
before_send=before_send,
short_message_limit=DEFAULT_LARK_TEXT_LIMIT,显式传参的意义不只是等价:它把「谁拥有 1200」写进代码。第一次尝试是全局删掉这个上限,结果正是这条通知路径的既有断言失败——所以最终版本把它收窄成显式归属,并补了一条正反两面的用例把两条路都钉住。
3. 管家答复路径(goal_topic_runtime.py / manager_returns.py / manager_reply_parts.py)——声明只受 provider 约束
三处都传 short_message_limit=None 并附一句说明:一条答复就是一条消息,分段是「provider 自己接不下」时的兜底。except 分支里的降级 + 分段 + 溢出提示完全没有改动。
对主干的风险
行为变化是单一方向且有意的:不带源消息的答复投递不再被 1200 切断,改为受 provider 的 150 KB 上限约束。通知路径保持原有紧凑上限,其既有断言仍通过,因此群通知不会被长正文污染。
超过 150 KB 的正文仍走原来的降级 + 分段 + 溢出提示;格式校验失败(字面 \n、畸形 <at>、字面 @)也仍走同一兜底,因此 format_unrepresentable 类事故没有被削弱。回滚只需 revert:没有新增持久化字段或 schema,老记录照常加载。
未验证的维度:150 KB 这个 provider 上限本身只在本地按字节校验,没有在真机上贴着边界发过;也没有覆盖 <150 KB 但接近上限的情形。富文本校验失败是另一条与本 PR 无关的降级原因,本次未改动。
我的整体评价
无阻断性发现。 「一条答复就是一条消息」应该是默认,而「因为自己猜的长度被切碎」是纯损失。这次把长度归属讲清楚:通知有通知的紧凑长度,答复只受 provider 上限,分段退化为真正的兜底。
验证:tests/extensions/ 982 passed;ruff 全绿;真机 1269 / 8244 字符单条投递读回确认;路由级与通知级两条用例从正反两面钉住边界。
作者是 PR 所有者,GitHub 不允许自我 approve,因此以 COMMENTED review 记录同一结论。这是控制面改动,保留给维护者决定合并,作者不自行合并。
English verdict: APPROVE - 4874@5855bb44b31d1636ccfd161155c04a5d6d91edef - the 1200 limit is a chat-root notification shape, not a provider bound (provider accepts 150 KB; live 1269- and 8244-character sends verified as sent_verified), so it is now an explicit parameter owned by the notification caller while the three answer deliveries declare the provider bound; the part sequence and the stall notice remain the fallback, with 982 passing extension tests and one-message answer delivery pinned by a route-level case.
动机
超长管家答复会被切成带
(N/M)前缀的多个分段,再把「完整答复保存在 LoopX 管家会话中」挂在最后一段上。这次实测确认:那个 1200 字符上限不是 provider 的限制,是我们自己的。证据链:
loopx/extensions/lark/outbound.py:DEFAULT_LARK_TEXT_LIMIT = 1200,而 provider 的真实上限是LARK_TEXT_REQUEST_MAX_BYTES = 150_000(150 KB,报错文案即 "exceeds the 150 KB provider limit")。reply_lark_event_inbox带已捕获源消息):782 / 1083 / 1269 / 8244 字符的正文都单条投递成功并读回确认(sent_verified、reply_verified: true)。问题出在把「通知形态的紧凑长度」当成了传输限制:任何不带源消息的投递都会继承 1200,于是本该一条发完的长答复被切断、交给分段序列。
改动思路
把这条长度从隐式继承改成显式归属:
short_message_limit成为_deliver_lark_inbox_outbound/reply_lark_event_inbox的显式参数,默认仍是 1200(不改变既有默认行为)。send_lark_inbox_message)显式声明它拥有这个紧凑上限——它是通道上的一条通知,不是一条答复,读者预期它短。None,声明只受 provider 的请求上限约束。具体改动
loopx/extensions/lark/inbox_reply.py:新增short_message_limit参数(默认DEFAULT_LARK_TEXT_LIMIT),在_deliver_lark_inbox_outbound内按None if source_event is not None else short_message_limit应用;send_lark_inbox_message显式传紧凑上限;reply_lark_event_inbox透传。loopx/extensions/lark/goal_topic_runtime.py:管家答复的单条发送传short_message_limit=None。loopx/extensions/lark/manager_returns.py:管家 return 投递传None。loopx/extensions/lark/manager_reply_parts.py:分段兜底发段时传None(段本身已按预算切好)。tests/extensions/test_lark_goal_topic_runtime.py::test_a_long_manager_answer_is_delivered_as_one_message(3000 字符 → 恰好一次+messages-reply、正文完整、落盘记录里没有delivery_part_count);tests/extensions/test_lark_inbox_reactions.py::test_an_answer_delivery_is_bounded_by_the_provider_not_by_the_notice_length(同一段超限正文在通知路径仍被拒,在答复路径被接受)。对主干的风险
行为变化是有意的、且只在一个方向上:不带源消息的答复投递不再被 1200 切断,改为受 provider 的 150 KB 上限约束。通知路径(
send_lark_inbox_message)保持原有紧凑上限不变,其既有断言仍然通过,因此群通知不会被长正文污染。超过 150 KB 的正文仍然走原来的降级 + 分段 + 溢出提示路径;格式校验失败(字面
\n、畸形<at>、字面@)也仍走同一兜底,因此format_unrepresentable类事故没有被削弱。未验证的维度:真机实测走的是
content_format="text"与 post 信封,没有对 150 KB 边界本身做真机发送(只在本地按字节校验);也没有覆盖 <150 KB 但接近上限的情形。我的整体评价
「一条答复就是一条消息」应该是默认,而「因为自己猜的长度被切碎」是纯损失——多一条消息、多一份分段记录、多一处读者要自己拼的地方。这次把长度归属讲清楚:通知有通知的紧凑长度,答复只受 provider 上限。分段与卡住通知保留为真正的兜底。
验证:
tests/extensions/982 passed;ruff全绿;真机 1269 / 8244 字符单条投递读回确认。English verdict: APPROVE - the 1200 limit is a chat-root notification shape, not a provider bound (provider accepts 150 KB; live 1269- and 8244-character sends verified), so it is now an explicit parameter owned by the notification caller while answer deliveries declare the provider bound; the part sequence and the stall notice remain the fallback, with 982 passing extension tests and pinned one-message delivery.