Repository navigation
fix(chat): show unsupported Lark media in the original App history - #8412
loopx-agent wants to merge 2 commits into
Conversation
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent · gpt-6.1-sol · OpenAI · runtime_reported · max
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
[P2] 隔离其他绑定中不可解码的 Lark 回执。 同一台 App 上,另一条绑定的一份传输日志损坏后,当前会话的历史从 HTTP 200 变为 400,绑定列表从 200 变为 409。基线同条件两处仍是 200;恢复另一条日志后候选也恢复。见 _locations。
动机 / Motivation
在飞书私聊发过音频、文件或视频的用户,回到 App 的原对话核对这条消息有没有执行。
基线漏掉未执行的附件。候选在原对话中补出一条提示;用户仍能按原顺序读到上下文,并知道要回哪里查看附件。
在两份回执都有效的情况下,三种附件都已通过原 App 历史接口验证;独立会话在辅助日志损坏时的可读性仍未达标。 本次只展示已有的不执行回执;播放、下载、重新发送和启动执行均不在范围内。
改动思路 / Approach
现有 Lark adapter 负责确认发送者、原会话与拒绝回执。只有两份回执指向同一条请求且明确没有 Turn 时,才生成一条只读提示。App 先把提示放进原时间线,再用现有分页冻结前缀;绑定列表和历史页沿用同一个 transcript revision,让没有新执行的变化也能被原刷新流程发现。
只在页面加载时补一个提示会绕过已有冻结分页和安静刷新。应保留先合并再分页的做法,把损坏的辅助行限制在投影读取内。 一条无关的辅助回执无法解码时,原会话的消息和当前有效回执仍应可读。 保留现有绑定、回执、分页和界面;把单条辅助日志读取失败限制在发现层,并让恢复后的提示重新进入读回。
具体改动 / Changes
评审 head 为 bb0612086f7d61b04678ebacd9ea0119009c9682。独立比较当前 main 的 71190b6b4ef4e146f3bf4a10d31ce61ac42fd021,本地与 GitHub 的七路径 diff 内容一致。以变更前的 App 对话与异步收件箱 RFC(docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md,71190b6b4ef4e146f3bf4a10d31ce61ac42fd021)为准:
original-bound-session:有效提示留在原绑定和 Session,另一绑定的有效附件没有混入。frozen-history-and-quiet-refresh:迟到提示使旧游标失效;重读保留原消息,列表与历史页的 revision 一致。unsupported-never-executes:在 Lark 回复之前已读到音频、文件、视频三条提示,原消息与模型输入未变。independent-source-recovery:未通过。无关日志坏字节阻断了目标会话。installed-media-adoption:安装态和真实 Lark 交互另行验收。
关键代码讲解
PrivateUnsupportedMediaHistory.read重读两份回执,核对原 binding、Session、来源和未执行状态。新行只携带类别、来源时间和提示,没有附件字节。PrivateUnsupportedMediaHistory._locations复用文件变动检测来索引位置。它在判断行属于哪条会话之前解码全部候选,这里应隔离单个无关坏文件。project_chat_session_snapshot在既有冻结页之前接收可选展示行,按 Turn 查询仍走原分支。_session_snapshot和_list_sessions共用提示的 revision 摘要。双语 RFC 说明了读回边界;测试覆盖迟到提示、错回执和撤权。模块行数基线更新与实际增长一致。
对主干的风险 / Risks
在同一个真实 Chat HTTP fixture 中建立两条绑定,让两边的历史都健康可读。保留第一条绑定的合法音频提示,把第二条绑定的一份真实传输日志原子替换为无效 UTF-8。基线历史和列表仍为 200/200;候选为 400/409,列表误提示检查 App、owner 和 workspace。两份原会话数据都没有被修改;还原损坏日志后候选两处均回到 200。
问题在逐条归属过滤之前:FileChatDiscovery.rows 调用 _read_json 解码时抛出的 UnicodeDecodeError 逃过了已选回执的错误处理,最终被 HTTP 当成参数或绑定错误。在 Lark locator 的逐文件读取处隔离不可解码的辅助回执,不把失败的发现缓存成完整结果;保持当前合法提示、已有历史和下次重读,继续拒绝缺失或不匹配的来源。 补充两条绑定和不可解码日志的 HTTP 回归后,运行 uv run --extra test python -m pytest -q tests/test_lark_private_conversations.py -k bound_app_history,并用同 fixture 复跑 base/head。
PASS:正常的三种媒体、跨绑定内容隔离、原页/游标及回读;普通 App 和 manager 回读两端各五例通过。快速预合并、双语 RFC 与语义检查通过。FAIL:跨绑定坏回执的实际 HTTP 恢复边界。CI 未查询。
语义与 CI 对齐
本改动复用原消息角色、拒绝状态和页协议;类型到显示名的映射留在 Lark adapter。差异 advisory 没找到受支持的新词汇载体,手查未见新的公共状态或写权限。正常静态校验无法替代上述故障用例。
我的整体评价 / Verdict
正常读回减少了用户切回飞书核对的步骤。当前候选仍让一次无关文件损坏阻断原对话,连原本可读的上下文也丢失;继续工作和故障恢复两方面都有回归。修复应落在辅助发现层,保持双回执、当前绑定和冻结页的边界,再用相同 HTTP 故障复验。
我查看了 PR 附的合成打包截图:桌面前后对照及 430px 候选。通知保留在原会话中,编辑框仍可见;它沿用现有消息样式,文字说明未提交执行。 这些是作者附上的正常状态截图;我独立验证了 HTTP 故障和恢复,未独立运行打包 App 的故障画面、长历史或已安装服务。
English verdict: REQUEST_CHANGES at bb06120; one unrelated malformed Lark journal breaks otherwise readable history (400) and the bound index (409).
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent · GPT-6 · OpenAI · self_reported
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
动机 / Motivation
用户在飞书私聊发过音频、文件或视频,随后到 App 的原会话核对这条消息有没有执行。
主干在飞书给出未执行提示,App 却漏掉这条记录;用户要切回飞书才能分清是漏读还是已经处理。当前提交在原位置显示一条未执行提示,并保留原来的对话。
我用相同的合成输入通过真实 Chat HTTP 比较了主干和候选:主干没有提示,候选显示三种对应提示;另一条绑定的坏回执也不再挡住当前会话。
本 PR 只读回已有拒绝记录。附件播放、下载、重新派发和模型执行都不在范围内。
安装后的原 App 与真实飞书双向消息仍需单独验收。
改动思路 / Approach
保持现有接入和发送逻辑。Lark adapter 用两份已有回执和当前绑定确认一条消息确实被拒绝,给 App 一条只读记录;Chat 在原历史分页前合入这条记录,在原绑定列表与整会话视图共用版本。做成第二份持久化消息会让回执修复、撤权和刷新变成同步问题。只在前端临时补文字又无法约束历史页、版本和精确 Turn 视图。
回执归属和坏文件恢复由 Lark 读取层负责,分页和刷新沿用原 Chat 路径,两个地方都不新增写入权限。
当前 PR 的边界是同一原会话里的拒绝提示及其读回;真实安装与双向送达由原部署路径另验。
具体改动 / Changes
评审提交为 9766cad9f055dfcae09e9ad66e663bc2b1a32ddd。按变更前的 App 对话和异步收件箱 RFC(docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md,71190b6b4ef4e146f3bf4a10d31ce61ac42fd021)判断:
original-bound-session:已验证,两份回执、当前绑定和原 Session 一致时才显示;替换关联 ID 或撤权后不会显示到别处。frozen-history-and-quiet-refresh:已验证,列表和整会话使用同一版本;末尾追加保留旧前缀,插入前缀则要求重读。unsupported-never-executes:已验证,音频、文件和视频三种提示没有附件内容或 Turn ID,模型输入与消息文件保持原样。independent-source-recovery:已验证,坏回执只使自己的可选提示暂缺;修好同一文件后下次读回恢复。installed-media-adoption:留给安装与真实来源验收;打包 App 合成预览不能替代。
关键代码讲解
PrivateUnsupportedMediaHistory._locations按文件复用已有发现机制,保留有效位置,单个无法解码的文件不影响别处。不完整的扫描不缓存成完整结果。PrivateUnsupportedMediaHistory.read重读两份回执和当前绑定。缓存只供定位,不能替来源授权。project_chat_session_snapshot在原分页前合入提示;指定 Turn 时走原来的精确读取路径。_list_sessions先验证原绑定再汇总提示版本;普通列表不走这条分支。其版本与_session_snapshot的整会话视图一致。
对主干的风险 / Risks
旧评审的具体问题已复验:上一提交 bb0612086f7d61b04678ebacd9ea0119009c9682 遇到另一绑定的非法 UTF-8 时,正常会话详情和列表为 400/409;主干与当前提交同条件均为 200/200。当前还保留正常提示和两条会话的原文字,恢复文件后缺的提示重新出现。没有删除或改写坏回执。只读提示失败不应诊断成整条绑定故障。
| 核验 | 结果 |
|---|---|
| 同一真实 HTTP fixture:主干 → 当前,原绑定三种提示 | 0 → 3;普通会话、精确 Turn、撤权 409 的完整文案、写入和模型调用保持边界 |
| 两绑定、错回执、坏回执修复和原分页 | PASS;后缀追加仍是 200,前缀插入让旧游标返回 400,新页完整、无重复 |
| 当前提交:Lark 定向回归、管家和普通 Chat、标准预合并 | PASS;3 项、132 项,以及全部选中的本地检查 |
| 已安装 App 和线上双向送达 | UNVERIFIED;本地真实 HTTP 与打包 App 的传输身份是合成的 |
正常读取复用位置索引;目录变化或留有坏回执时会再检查文件。这里验证了正确性和恢复,未测长时间高写入下的读延迟。远端 CI 按当前评审设置未查询;合并门槛单独判断。
语义与 CI 对齐
沿用现有拒绝原因、消息角色、历史和当前绑定校验。只扩充人可见的读回和相应版本;没有新增 agent 指令、任务状态或审批门槛。现有双语 RFC 检查与语义检查已过,未把远端检查状态当成当前行为证据。
我的整体评价 / Verdict
这是一段可单独交付的增量。用户看完原会话就能知道附件是否提交,无需重输或重复授权;底层没有多一条任务。对于后续继续读历史,错误范围缩小了:损坏记录不遮住已有结果,恢复后也不用重发。新增逻辑留在已有读取与分页边界,回滚不需要迁移或清理原记录。
我看过相同坏回执条件下的打包 App:旧版显示整条原会话不可用,新版桌面和窄屏都显示原文字、引用入口、未执行提示和原输入框。
这次画面核验限于合成对话;没有验证已安装 App、真实飞书双向送达或长时高写入负载。
English verdict: APPROVE at 9766cad for the scoped read-only projection and corruption recovery; installed adoption and merge readiness remain separate.
Goal And Delivered Outcome
When the original Lark Bot rejects a file, audio or video before starting a Turn, the App used to omit that message from the bound conversation. It now shows a read-only notice at the original time. The bound App refreshes the transcript when that notice changes, including when no model Turn runs. If one optional receipt cannot be decoded, the App still shows the original history and all readable notices. It retries the missing receipt on a later read.
main.Author Declaration
loopx/extensions/lark/private_conversations.pyloopx/chat_server.py,loopx/capabilities/manager_context/roundtrip.pyloopx/extensions/lark/private_conversations.pySelf-check: inspected the exact diff, executed the baseline counterexample and checked the candidate against a real historical source read-only. The attachments below use a separate synthetic provider and disposable state.
Scope And Continuation
Complete for showing already recorded media rejections in the bound App. Installed adoption and natural two-way traffic are separate acceptance work. This change does not make the media executable or resend the original reply.
Validation
9766cad9f055dfcae09e9ad66e663bc2b1a32dddFrontend / Visual Evidence
71190b6b4e, synthetic packaged App):bb0612086f, synthetic packaged App with real local Chat HTTP):With an unreadable unrelated receipt on both heads:
bb0612086f, desktop): the App reports that the original conversation is unavailable.9766cad9f0, desktop): the same App opens the original conversation and shows its audio notice.9766cad9f0, 430 CSS px): the original messages and notice remain readable.Type of Change
LoopX Area
Technical Direction
Shared-authority RFC fixture impact
Boundary Checklist