自动化程序最容易报告的结果是“按钮已经点击”。业务最关心的结果却是“正确消息已经进入正确买家的会话”。这两个结果之间,可能隔着窗口切换、异步发送、协议失败、数据库写入和多实例竞争。
在多店铺智能客服场景中,真正需要防范的不是少回复一条消息,而是把内容发给错误的人。因此,系统设计必须从业务终态倒推,而不是从 UI 操作开始顺推。
消息源不是一个简单列表
千牛客户端的历史消息存储在 SQLCipher 数据库中,并按会话哈希分散到 64 张表。不同机器和卖家对应不同密钥,普通 SQLite 客户端无法直接读取。
我通过 Frida hook sqlite3_key 获取当前实例实际使用的密钥,再使用 ctypes 调用原生 SQLite。读取端不是每次扫描全部数据,而是维护分片游标,按会话增量轮询,并以 mid 去重。
每条进入系统的消息必须先绑定三个身份:
- 当前客户端实例;
- 所属店铺;
- 对应买家会话。
如果身份关系没有在消息进入时确定,后续无论模型回答得多准确,都存在跨会话风险。
队列边界决定隔离强度
最初看起来可以建立一个全局回复队列,但全局队列无法表达店铺窗口和买家会话的切换约束。更可靠的方式是按“店铺 + 买家”建立串行 worker:同一会话内保持顺序,不同会话之间可以受控并发。
队列任务携带完整上下文,包括消息 ID、店铺、买家、模型会话和重试次数。人工接管时,只暂停对应业务身份的自动回复,不影响其他店铺。
这套边界还需要覆盖异常路径:
- 模型超时,进入降级回复或人工队列;
- 客户端窗口切换失败,不执行发送;
- 输入框类型变化,在 Edit 与 CEF 路径之间选择适配器;
- 同一消息重复出现,由
mid去重; - 多实例同时运行,以实例和店铺双重隔离。
UI 成功只是中间状态
pywinauto 可以定位窗口、切换会话并写入 Edit 输入框。CEF 页面则需要另一套定位或浏览器控制方式。但无论哪条 UI 路径,点击发送之后都不能立即把任务标记为完成。
可靠链路会继续查询消息数据库,检查以下事实:
- 新记录是否已经落库;
- 记录是否属于目标买家会话;
- 内容摘要是否与本次发送一致;
- 消息时间是否位于本次任务窗口;
- 是否出现失败状态或重复记录。
只有这些条件满足,队列才确认业务成功。否则进入有限重试、降级或人工接管。
协议发送同样需要终态验证
飞鸽场景并不完全依赖 UI。我通过 Hook webpack IM 模块捕获协议消息,分析 Pigeon protobuf 与 pigeon_sign,再用 curl_cffi 接入接口收发链路。
协议调用能够减少界面不稳定性,但“HTTP 返回成功”仍不是终态。接口可能接受请求却在异步处理阶段失败,因此协议发送后仍要观察消息列表或本地存储中的最终记录。
这说明终态验证与发送通道无关:UI 和协议只是两种动作执行器,业务成功条件应该保持一致。
把可诊断性一起交付
自动化系统上线后,最难处理的问题通常不是稳定复现的异常,而是“某个店铺偶尔没有回复”。如果系统只记录最后一条错误,很难判断问题发生在消息读取、模型生成、窗口切换、协议调用还是落库确认。
因此我会为每个任务记录阶段状态和关联 ID,并提供一键诊断:
message_observed
→ context_bound
→ answer_generated
→ send_dispatched
→ final_state_confirmed
停在哪一步,就检查对应证据。密钥缓存、限流、重试、人工接管和诊断记录不是附属功能,它们共同决定工具能否真正交给运营团队使用。
自动化的完成条件不应该由动作定义,而应该由可再次观察的业务状态定义。只有当系统能够回答“发给了谁、是否到达、如何证明”,一次自动回复才算真正结束。