跳到正文
← 返回博客
FIELD NOTE / 工程实践

自动回复的终点,不是点击“发送”

从 64 张加密消息分片表到数据库二次确认:如何把客服自动化从 UI 动作推进到可验证的业务闭环。

自动化程序最容易报告的结果是“按钮已经点击”。业务最关心的结果却是“正确消息已经进入正确买家的会话”。这两个结果之间,可能隔着窗口切换、异步发送、协议失败、数据库写入和多实例竞争。

在多店铺智能客服场景中,真正需要防范的不是少回复一条消息,而是把内容发给错误的人。因此,系统设计必须从业务终态倒推,而不是从 UI 操作开始顺推。

消息源不是一个简单列表

千牛客户端的历史消息存储在 SQLCipher 数据库中,并按会话哈希分散到 64 张表。不同机器和卖家对应不同密钥,普通 SQLite 客户端无法直接读取。

我通过 Frida hook sqlite3_key 获取当前实例实际使用的密钥,再使用 ctypes 调用原生 SQLite。读取端不是每次扫描全部数据,而是维护分片游标,按会话增量轮询,并以 mid 去重。

每条进入系统的消息必须先绑定三个身份:

  • 当前客户端实例;
  • 所属店铺;
  • 对应买家会话。

如果身份关系没有在消息进入时确定,后续无论模型回答得多准确,都存在跨会话风险。

队列边界决定隔离强度

最初看起来可以建立一个全局回复队列,但全局队列无法表达店铺窗口和买家会话的切换约束。更可靠的方式是按“店铺 + 买家”建立串行 worker:同一会话内保持顺序,不同会话之间可以受控并发。

队列任务携带完整上下文,包括消息 ID、店铺、买家、模型会话和重试次数。人工接管时,只暂停对应业务身份的自动回复,不影响其他店铺。

这套边界还需要覆盖异常路径:

  • 模型超时,进入降级回复或人工队列;
  • 客户端窗口切换失败,不执行发送;
  • 输入框类型变化,在 Edit 与 CEF 路径之间选择适配器;
  • 同一消息重复出现,由 mid 去重;
  • 多实例同时运行,以实例和店铺双重隔离。

UI 成功只是中间状态

pywinauto 可以定位窗口、切换会话并写入 Edit 输入框。CEF 页面则需要另一套定位或浏览器控制方式。但无论哪条 UI 路径,点击发送之后都不能立即把任务标记为完成。

可靠链路会继续查询消息数据库,检查以下事实:

  1. 新记录是否已经落库;
  2. 记录是否属于目标买家会话;
  3. 内容摘要是否与本次发送一致;
  4. 消息时间是否位于本次任务窗口;
  5. 是否出现失败状态或重复记录。

只有这些条件满足,队列才确认业务成功。否则进入有限重试、降级或人工接管。

协议发送同样需要终态验证

飞鸽场景并不完全依赖 UI。我通过 Hook webpack IM 模块捕获协议消息,分析 Pigeon protobuf 与 pigeon_sign,再用 curl_cffi 接入接口收发链路。

协议调用能够减少界面不稳定性,但“HTTP 返回成功”仍不是终态。接口可能接受请求却在异步处理阶段失败,因此协议发送后仍要观察消息列表或本地存储中的最终记录。

这说明终态验证与发送通道无关:UI 和协议只是两种动作执行器,业务成功条件应该保持一致。

把可诊断性一起交付

自动化系统上线后,最难处理的问题通常不是稳定复现的异常,而是“某个店铺偶尔没有回复”。如果系统只记录最后一条错误,很难判断问题发生在消息读取、模型生成、窗口切换、协议调用还是落库确认。

因此我会为每个任务记录阶段状态和关联 ID,并提供一键诊断:

message_observed
→ context_bound
→ answer_generated
→ send_dispatched
→ final_state_confirmed

停在哪一步,就检查对应证据。密钥缓存、限流、重试、人工接管和诊断记录不是附属功能,它们共同决定工具能否真正交给运营团队使用。

自动化的完成条件不应该由动作定义,而应该由可再次观察的业务状态定义。只有当系统能够回答“发给了谁、是否到达、如何证明”,一次自动回复才算真正结束。