先说结论
Odoo 的网站在线聊天,从来不是“网页里放一个聊天浮窗”这么简单。
website_livechat 真正在处理的,是一条完整的访客沟通链路:
- 先把网站访问者识别成
website.visitor; - 再把聊天会话落到
discuss.channel; - 坐席可以被动接待,也可以主动发起 chat request;
- 访客换设备、刷新页面、从匿名变成已登录用户时,系统还要尽量把身份和会话重新串起来。
所以这个模块的核心价值,不是“能聊天”,而是:
让网站访客、客服坐席和历史上下文尽可能保持同一条业务线。
这也是为什么源码里同时出现了 website.visitor、discuss.channel、guest、operator、history 这些看起来横跨多个子系统的对象。
一、为什么 Odoo 先管理“访客”,再管理“聊天”
在 website_livechat/models/website_visitor.py 里,Odoo 给 website.visitor 扩展了几类关键字段:
- 当前正在对话的坐席
livechat_operator_id - 访客关联的聊天会话
discuss_channel_ids - 有消息记录的会话数量
session_count
这说明在 Odoo 眼里,聊天并不是孤立窗口,而是附着在“访客对象”上的互动历史。
这有几个直接后果:
- 客服看到的不是一条消息,而是一个访客。
- 访客关闭窗口后,历史会话还能回挂到同一个 visitor。
- 后续做转化、回访、统计时,可以从 visitor 维度继续延展。
很多轻量客服工具只关心 message stream;Odoo 则更像把在线聊天做成了网站访客运营的一部分。
二、为什么“主动邀聊”本质上是在创建待激活会话
action_send_chat_request() 很值得看。
它不是简单给前端弹个窗,而是先做几件非常实在的事:
- 检查访客当前是否已在活跃聊天中,避免重复邀聊;
- 检查当前网站是否配置了可用的 livechat channel;
- 把当前用户加入该频道的可服务用户里;
- 创建
discuss.channel,并标记is_pending_chat_request = True; - 给该会话写入
livechat_operator_id、livechat_visitor_id、国家、展示名等上下文; - 必要时为匿名访客补一个
mail.guest成员。
这里最关键的点是:
主动邀聊不是“即时发送一条前端通知”,而是“先把一条可落地的聊天会话建好,再等访客在网站端接住它”。
因此 Odoo 的主动聊天更稳定,也更适合和后续会话记录、客服工作台联动。
三、为什么访客下一次打开页面时,邀聊还能被“重新捡起来”
controllers/webclient.py 里的 _link_visitor_to_livechat() 解释了这个机制。
当网站端初始化 livechat 时,系统会去找:
- 当前请求对应的
website.visitor - 同一网站 livechat channel
- 还没结束的会话
livechat_end_dt = False - 已经有消息
has_message = True - 但仍处于待访客接收状态
is_pending_chat_request = True
如果命中,Odoo 就会把这条 chat request 重新挂回当前访客上下文,并打开聊天窗口。
这意味着什么?
- 坐席先发起邀聊,访客不一定当场回应;
- 访客只要继续浏览网站,系统就有机会把这条请求补挂回来;
- 这不是纯前端临时态,而是服务端维护的“待接管会话”。
对业务来说,这个设计非常重要:
客服动作不会因为用户刷新一下页面就全部丢失。
四、为什么 Odoo 还要区分匿名 guest 和已识别 partner
源码里有一个很容易被忽视的细节:匿名访客和已登录用户的会话绑定方式并不一样。
如果访客尚未登录,系统可能通过 mail.guest 挂接到 livechat channel;
如果访客后来被识别、登录,或者 visitor 被重新 upsert/merge,Odoo 会尝试把旧会话重新关联到新的 visitor 或 partner。
比如:
_upsert_visitor()会把 guest 现有 livechat channel 回写到新 visitor;_merge_visitor()会把次级访客历史合并到主访客;- 合并时还会把频道成员从公共访客调整成真正 partner。
这背后的产品思路很清晰:
网站沟通往往从匿名开始,但业务转化需要尽量走向可识别身份。
Odoo 不会假设用户一开始就愿意登录,但它又尽量不让匿名阶段的聊天历史白白蒸发。
五、为什么客服看到“最近访问页面”非常关键
_get_visitor_history() 会从 website.track 里抓取最近访问的页面,并在会话 store 中返回给客服端。
这不是锦上添花,而是客服效率的核心上下文。
因为对坐席来说,下面这些问题常常决定回复质量:
- 访客刚刚看的是产品页、价格页还是帮助页?
- 他是不是反复在某一个页面停留?
- 这是第一次接触,还是已经来回浏览几次?
如果客服只能看到一句“你好,在吗”,判断几乎全靠猜。 而如果客服能看到最近访问轨迹,回复就会从通用客服话术,变成更像销售顾问或解决方案咨询。
这也是为什么 Odoo 不是只存 message,还要把 visitor history 一并塞进 store。
六、为什么空会话会被清理掉
discuss.channel.channel_pin() 的覆写也很有意思。
如果这是一个 livechat channel,坐席取消固定/关闭窗口,而且频道里还没有消息,系统会直接把这个空频道删掉。
这背后的逻辑非常务实:
- 主动邀聊可能创建了会话;
- 但如果坐席根本没真正开聊;
- 留下一个空壳频道,只会污染后续可用状态判断;
- 甚至会影响再次向同一访客发起邀请。
所以 Odoo 干脆把“没有实际对话内容的空 livechat 会话”视为可回收对象。
这说明它不是把 channel 当作永恒记录,而是把它当成一个要为业务流程服务的会话容器。
七、为什么 visitor last visit 会随着消息流更新
message_post() 的扩展里有个小而关键的动作:
如果消息不是由操作员发出,而是访客或系统侧发出,Odoo 会更新 visitor 的最后活跃时间。
这能带来两个实际价值:
- 客服列表里访客活跃度更真实。
- 运营判断“人还在不在页面上”时更靠谱。
这类细节虽然不显眼,但会直接影响客服体验。 因为在线聊天最大的敌人之一,就是“数据看起来在线,实际上人早走了”。
八、实施时最容易踩的坑
如果你要把 Odoo 在线聊天用于官网获客,不要只盯着按钮样式,真正容易出问题的是这些地方:
1. 网站和 Livechat Channel 没有配干净
源码明确要求网站上要有 channel_id,否则坐席连主动邀聊都发不出去。
2. 只关注客服对话,不关注 visitor 身份连续性
如果访客跨页面、跨会话、从匿名到登录的链路没想清楚,你会以为“聊天数据都在”,其实业务上下文已经断了。
3. 把主动邀聊理解成前端弹窗,而不是待激活会话
这样会低估服务端状态管理的重要性,也会误判为什么刷新后还能接住会话。
4. 忽略最近页面历史
客服效率差,往往不是不会回复,而是根本没有上下文。
最后总结
Odoo 的 website_livechat 真正解决的问题,不是“网站能不能聊天”,而是:
- 能不能把网站访客沉淀成可持续识别的对象;
- 能不能让客服主动发起接触,而不是只等用户先开口;
- 能不能在匿名、刷新、重进页面、后续登录这些变化里,尽量保住会话连续性;
- 能不能让客服带着访问轨迹去沟通,而不是盲聊。
所以如果你把它看成一个聊天插件,很容易低估它。
更准确地说,它是:
Odoo 在官网侧把“访客识别 + 客服会话 + 上下文延续”合并成的一套互动路由系统。
这也解释了为什么它在源码层面既碰 visitor,又碰 discuss,又碰 guest 与会话清理逻辑——因为真正难的,从来不是发出一条消息,而是让整条沟通链不断线。
DISCUSSION
评论区