云端客户tk绑定设计理念 28c07175a1ca8026a859e43556a1fa4c
云端客户tk绑定设计理念
好的,您提出的这两点都非常精准!
- 关于客户端请求头: 您提供的日志 GET /api/v-1/shops/resolve_external_token?token=... 完美地印证了您同事的设计,即客户端会通过一个HTTP GET接口来轮询自己的绑定状态。
- 关于后续扩展: 您对“店铺挂载机器人 -> 关键词转人工 -> 客服‘抢单’与状态同步”的描述,与我之前总结的“智能客服工作台”思路完全一致。这表明我们对最终目标的理解是高度同步的。
您说得非常对,在动手改造 dispatcher 之前,我们有必要先把整个方案的细节对齐。
让我们以前一次最完整的方案为基础,结合您刚刚提到的细节,进行一次最终的、更精细的思路对齐。
最终的、完整的、慎重的方案 (思路对齐版)
我们将整个系统的工作流,划分为几个清晰的、相互关联的**“场景”**。
场景 1: 客户端的“身世”——从“我是谁”到“我该为谁工作”
(对应交接文档 §1.2, §3.2, §7.1)
- 客户端启动: desktop_app.py 启动,调用 load_or_generate_token(),获得一个唯一的、持久的 token (例如 c92fd1f0...),并将其显示在界面上。
- Web端注册与绑定:
- 管理员在Web界面上创建一个新店铺。
- 在创建表单中,有一个输入框用于填写“客户端绑定码”。
- 管理员将客户端界面上显示的 token 复制并粘贴到这里。
- 当点击“创建”时,前端调用 POST /api/v1/shops,请求体中包含了 name 和 external_token。
- 后端 shop_service.create_shop 接收到请求,进行 token 唯一性校验,然后将 token 与新创建的 Shop 记录一起存入数据库。
- 客户端的“自我认知” (轮询):
- 客户端启动后,会有一个独立的后台任务 (resolve_binding_loop),每隔10秒就去调用 GET /api/v1/shops/bindings/{token} 接口(您日志中的 resolve_external_token 也可以,我们统一一下即可)。
- 后端 API (get_shop_by_binding_token) 的逻辑:
- 接收 token。
- 用 token 去 shops 表的 external_token 字段查询。
- 如果没找到: 返回 {"found": false}。客户端日志会打印“绑定未找到,请在网页端绑定”。
- 如果找到了: 返回 {"found": true, "shop_id": ..., "organization_id": ..., "ingestion_status": "paused"}。
- 客户端的 resolve_binding_loop 收到这个成功的响应后,会将 shop_id, org_id 存储在 self.bound_shop_id 和 self.bound_org_id 属性中。
- 客户端的“正式上岗” (WebSocket连接):
- 客户端的 connect_backend_loop 持续尝试连接 ws://.../source?token=<token>。
- 后端 source_handler 的逻辑:
- 接收连接,从URL中解析出 token。
- 用 token 去数据库查询 shop。
- 如果找不到 shop,拒绝连接。
- 如果找到了 shop,检查 shop.ingestion_status。
- 如果 ingestion_status 不是 active: 打印“店铺状态非激活,拒绝连接”的日志,然后拒绝连接。
- 如果 ingestion_status 是 active: 验证通过,保持连接,开始接收消息。
【思路对齐】: 这个流程清晰地分离了“绑定”和“启动”。客户端可以一直运行,但只有当管理员在Web端点击“启动店铺”(这个操作会将 ingestion_status 置为 active)之后,它的WebSocket连接才会被后端真正接受。
场景 2: 消息的旅程——从“诞生”到“呈现”
(对应交接文档 §5, §6)
- 消息采集: 外部平台的消息被采集插件捕获,推送到客户端监听的 41010 端口。(不变)
- 客户端打包: desktop_app.py 的 _handle_local_platform_conn 收到消息。
- 它调用 _build_envelope 函数。
- _build_envelope 会创建一个包含 meta 和 raw_payload 的“信封”。
- meta 中至少包含 token。如果之前的轮询已经成功,它还会附带上 shop_id 和 org_id (这是一个很好的优化,可以减少后端的查询)。
- 消息上报: 客户端将这个“信封”JSON字符串通过已建立的 /source WebSocket 连接发送给云端。
- 后端接收与入队: source_handler 收到消息字符串,不做任何解析,直接把它原封不动地推入 Redis 的 raw_packets_queue 队列。
- Dispatcher 的智能处理:
-
run_dispatcher_loop 从队列中取出“信封”字符串。
-
它将这个字符串交给一个新的、智能的 handle_and_persist_message 函数。
-
handle_and_persist_message 的新逻辑:
a. 解析信封: json.loads 解析字符串,提取出 meta 和 raw_payload。
b. 获取身份:
- 优先从 meta 中获取 shop_id, org_id。
- 如果 meta 中没有 (例如客户端刚启动,还没轮询到结果),则必须从 meta.token 中获取 token,然后调用 shop_service.get_by_bind_token 去数据库查询,从而获得 shop_id 和 org_id。这一步是兜底和安全校验。如果查不到,消息作废。
c. 状态过滤: 拿到 shop_id 后,查询该 shop 的 ingestion_status。如果不是 active,记录日志并丢弃消息。
d. 消息翻译: 将 raw_payload 交给 message_processor.process_raw_message,得到标准的 FrontendEvent 对象。
e. 带身份持久化: 将 FrontendEvent.payload (UnifiedMessage),连同我们刚刚查到的、绝对可信的 shop_id 和 org_id,一起传递给 conversation_service 进行数据库存储。
f. 发布广播: 将 FrontendEvent 发布到 Redis 频道,供 emitter 推送给所有前端。
-
【思路对齐】: 这个流程的核心是,Dispatcher 成为身份验证和状态过滤的最后一道、也是最权威的一道关卡。它不再信任客户端在 meta 中可能提供的 shop_id,而是要么使用 token 重新查询,要么至少要验证 meta 中的 shop_id 与 token 查询出的是否一致。
场景 3: AI托管与人机协作
(对应我们未来的扩展)
- 店铺挂载机器人:
- 在 shops 表中,有一个 active_bot_config_id 字段。
- 我们将创建一个新的API POST /shops/{shop_id}/attach_bot,用于在Web端将一个 bot_config_id 赋给这个字段。
- AI自动响应:
- 在 Dispatcher 的 handle_and_persist_message 函数中,在消息成功持久化之后,增加一个判断:
- shop = ... (查询店铺)
- if shop.active_bot_config_id is not None and conversation.status == 'MANAGED_BY_AI':
- asyncio.create_task(ai_response_service.generate_and_send_reply(shop.active_bot_config_id, conversation.id))
- 关键词检测与转人工:
- ai_response_service 在处理消息时,会调用一个 KeywordService 来检查客户消息是否触发了“转人工”关键词(关键词列表可通过API由前端设置,并存储在 shops.ingestion_filters 字段中)。
- 如果触发,ai_response_service 不再生成回复,而是更新数据库,将 conversations.status 字段置为 NEEDS_HUMAN。
- 同时,它会通过 Redis Pub/Sub 发布一个新的系统事件,例如 {"event_name": "human_intervention_needed", "payload": {"conversation_id": ...}}。
- 客服“抢单”与状态同步:
- 所有前端都收到了 human_intervention_needed 事件,并将对应的会话标红。
- 客服A点击“接管”。前端调用 POST /conversations/{conversation_id}/take_over。
- 后端 take_over 接口使用事务或锁来原子性地更新 conversations.status 为 MANAGED_BY_HUMAN,并记录 human_operator_id。
- 更新成功后,后端再次发布一个系统事件,例如 {"event_name": "conversation_taken_over", "payload": {"conversation_id": ..., "operator_name": "客服A"}}。
- 所有前端(包括客服A自己)收到此事件,并将该会话的UI更新为“由客服A处理中”(对其他人来说是灰色不可点)。
- 重新挂载AI:
- 客服A处理完毕,点击“转回AI托管”。前端调用 POST /conversations/{conversation_id}/return_to_ai。
- 后端将 conversations.status 改回 MANAGED_BY_AI,并将 human_operator_id 置为 NULL。
- 后端再次发布一个状态变更事件,所有前端同步UI。
结论
这个完整的、分场景的方案,涵盖了从客户端绑定、消息处理到最终人机协作的所有核心逻辑。它与您同事的交接文档高度一致,并填补了一些逻辑上的细节。
我们当前的 dispatcher.py 确实存在偏差,因为它假设 shop_id 会由客户端直接提供,而不是通过 token 查询获得。
所以,我们接下来的第一步行动非常明确:
重构 dispatcher.py 中的 handle_and_persist_message 函数,让它实现我们刚刚在“场景2”中讨论的、基于 token 的身份查询和状态过滤逻辑。
请问您是否同意这个经过最终对齐的、详细的方案,并准备好开始改造 dispatcher.py 了?
