深入解析 Odoo 在线客服模块 (im_livechat)
Odoo Livechat 是一款集成于 Odoo 平台的实时在线客服系统,它赋予用户在网页界面上直接与客服人员进行即时沟通的能力。本文将逐步剖析 Livechat 的实现细节,从入口模板文件的加载机制,到后端初始化逻辑,再到前端客服服务的构建与交互,全方位揭示其内部运作机制。
相关功能介绍见:Odoo讨论+聊天模块
1. 入口模板文件:集成与数据传递
- 网站与在线客服频道关联配置
每个网站可设置一个 im_livechat.channel
实例作为在线客服频道,通过字段 channel_id = fields.Many2one(‘im_livechat.channel’) 进行关联。此配置决定了特定网站的在线客服参数及客服机器人运行规则。
- 模板集成与数据获取
在 website_livechat/views/website_livechat.xml
文件中, Livechat 被无缝集成到 Odoo 网站模板中。具体操作如下:
- 条件加载:通过
<t t-if="not no_livechat and website and website.channel_id">
判断语句,确保 Livechat 只在未被禁用且已配置频道的页面上加载。 - 数据传递:调用 website._get_livechat_channel_info() 函数获取频道信息,并将其传递给 im_livechat.loader 模板, 初始化
odoo.__session_info__.livechatData
。
<!--Integrate Livechat in Common Frontend for Website Template registering all the assets required to execute the Livechat from a page containing odoo-->
<template id="loader" inherit_id="website.layout" name="Livechat : include loader on Website">
<xpath expr="//head">
<!-- Odoo机器人测试页面 chatbot_test_script_page 通过 <t t-set="no_livechat" t-value="True"/> 设置 no_livechat=True。调查问卷相关页面也设置 no_livechat=True。 -->
<t t-if="not no_livechat and website and website.channel_id">
<script>
<t t-call="im_livechat.loader">
<t t-set="info" t-value="website._get_livechat_channel_info()"/>
</t>
</script>
</t>
</xpath>
</template>
- 后端数据初始化
在 Python 后端 im_livechat/models/im_livechat_channel.py
,通过 livechatData = get_livechat_info()
函数,初始化并返回包含在线客服状态、服务器URL等关键信息的数据对象。其中,判断在线客服是否可用的依据是频道中设置的聊天机器人脚本数量或可用客服人数:
info['available'] = self.chatbot_script_count or len(self.available_operator_ids) > 0
- 前端数据注入
模板文件 im_livechat/views/im_livechat_channel_templates.xml
负责将后端获取的 Livechat 数据注入到前端 JavaScript 环境:
<!-- initialize the LiveSupport object -->
<template id="loader" name="Livechat : Javascript appending the livechat button">
<t t-translation="off">
odoo.__session_info__ = Object.assign(odoo.__session_info__ || {}, {
livechatData: {
isAvailable: <t t-out="'true' if info['available'] else 'false'"/>,
serverUrl: "<t t-out="info['server_url']"/>",
options: <t t-out="json.dumps(info.get('options', {}))"/>,
},
});
</t>
</template>
2. 前端客服相关核心服务详解
在前端主要依赖以下三个核心服务:
- mail.thread: import(“@mail/core/common/thread_service”).ThreadService
- im_livechat.livechat: import(“@im_livechat/embed/common/livechat_service”).LivechatService
- im_livechat.chatbot: import(“@im_livechat/embed/common/chatbot/chatbot_service”).ChatBotService
im_livechat/livechat_service.js
文件在前端注册im_livechat.livechat
服务:
首先将后端传递的 options 和 isAvailable 状态赋值给 LivechatService 类
,并实例化。接着,根据服务的可用性决定是否执行初始化操作。若服务可用,调用 LivechatService.initialize()
方法,发起 RPC 调用 /im_livechat/init
,传入当前频道 ID,并在响应后标记服务为已初始化。最后,将 im_livechat.livechat
服务注册到前端服务注册表中。
伪代码如下:
LivechatService.options = session.livechatData.options
LivechatService.available = session.livechatData.isAvailable;
livechat = new LivechatService()
if (livechat.available) {
LivechatService.initialize()
-> this.rpc("/im_livechat/init", {channel_id})
-> livechat.initialized = true
}
registry.category("services").add("im_livechat.livechat");
mail/thread_service.js
在前端注册mail.thread
服务:
export const threadService = {
dependencies: [
"mail.store", "orm", "rpc", "notification", "router", "mail.message",
"mail.persona", "mail.out_of_focus", "ui", "user", "im_livechat.livechat", "im_livechat.chatbot"
],
/**
*@param {import("@web/env").OdooEnv} env
* @param {Partial<import("services").Services>} services
*/
start(env, services) {
return new ThreadService(env, services);
},
};
registry.category("services").add("mail.thread", threadService);
3. 在线聊天按钮组件
LivechatButton
组件负责呈现在线聊天按钮并响应用户点击事件。点击按钮时,执行以下操作:
- 调用
this.threadService.openChat()
打开聊天窗口。 - 使用 getOrCreateThread({ persist = false }) 通过 RPC 调用
/im_livechat/get_session
获取或创建临时会话信息。此时,由于 persist = false,仅返回type='livechat'
的临时会话,不立即创建discuss.channel
记录。用户发送消息时会创建discuss.channel
记录。 - 如果聊天机器人可用则启动 chatbotService 服务。
伪代码如下:
export class LivechatButton extends Component {
static template = "im_livechat.LivechatButton"
onClick -> this.threadService.openChat()
-> thread = getOrCreateThread({ persist = false }) -> rpc("/im_livechat/get_session")
-> if (this.chatbotService.active) chatbotService.start()
}
4. 后端创建会话
当 Livechat 频道支持机器人且存在 chatbot_script
时,RPC 调用 /im_livechat/get_session
会触发 _get_livechat_discuss_channel_vals()
方法,用于在后端创建相应的会话记录。
5. 聊天窗口
- 聊天机器人处理,伪代码如下:
// 无需用户回复时,直接触发下一步
chatBotService.start() -> _triggerNextStep() -> _getNextStep() -> this.rpc("/chatbot/step/trigger")
// 点击推荐回复选项时, 发送对应的回复
onclick -> this.answerChatbot(answer) -> this.threadService.post(answer)
- 后端处理用户回复答案并返回下一步:
# 调用处理用户回复路由:/chatbot/step/trigger, 处理存储用户回复并返回下一步对话
chatbot_trigger_step() -> next_step = _process_answer(channel, answer)
-> if step_type in ['question_email', 'question_phone'] chatbot_message.write({'user_raw_answer': message_body}) # 存储用户回复
-> _fetch_next_step() -> 'chatbot.script.step' # 下一步数据
# 处理下一步,如果下一步为切换到真人客服,则自动添加相关人员到频道中
posted_message = next_step._process_step(discuss_channel)
-> if self.step_type == 'forward_operator' -> _process_step_forward_operator(discuss_channel)
-> discuss_channel.add_members()
-> channel._chatbot_post_message() -> message_post() # 发送消息
- 用户输入检测
使用 useEffect 监听用户输入变化,调用 self.detect() 和 getSupportedDelimiters() 检测是否包含特定指令字符(如 @、# 或 :)。
// 用户输入时检测是否包含指令字符
useEffect(() => self.detect() -> getSupportedDelimiters(),
() => [selection.start, selection.end, textInputContent]
);
// 输入 @ 或 # 时触发搜索联系人或频道
suggestionService.fetchSuggestions(self.search)
// 点击推荐列表项设置 search 条件,例如选择 '/help'
onSelect: this.suggestion.insert(option) -> this.search = {delimiter: "/", position: 0, term: "help "}
// im_livechat 模块扩展 suggestionService 模块,支持冒号 ':' 命令
getSupportedDelimiters(thread) {
// 仅 livechat 频道支持通过 ':' 搜索内置的快速回复
return thread?.model !== "discuss.channel" || thread.type === "livechat"
? [...super.getSupportedDelimiters(...arguments), [":"]]
: super.getSupportedDelimiters(...arguments);
},
6. 前端发送消息
前端发送消息时执行以下操作:
- 调用 post(thread, body) 方法,传入会话 thread 和消息内容 body。
- 如果 thread.type 为 “livechat”,调用 livechatService.persistThread() 生成 ‘discuss.channel’ 记录。
- 如果消息内容以 / 开头,识别为命令。从命令注册表 commandRegistry 中获取命令 command。如果找到命令,执行 executeCommand(command) 并返回结果;否则继续消息发送流程。
- 否则,通过 RPC 调用 /mail/message/post 将消息发送到服务端。
- 触发 chatbotService 上的 MESSAGE_POST 事件监听器。
伪代码如下:
post(thread, body) -> if (thread.type === "livechat") livechatService.persistThread()
-> if (body.startsWith("/")) -> command = commandRegistry.get()
-> if (command) executeCommand(command) return; // 执行命令返回结果
-> else this.rpc('/mail/message/post') // 发送消息到服务端
-> this.chatbotService.bus.trigger("MESSAGE_POST") // 触发 chatbotService 监听器
7. 后台消息处理
- 路由
/mail/message/post
处理函数 mail_message_post, 根据 thread_id 查找对应的频道(discuss.channel)实例。调用 message_post() 发布消息到频道, 存储消息并根据需要通过多种方式发送通知(如短信、Web 推送、Inbox、电子邮件等)。其中 _message_post_after_hook(new_message, msg_values)是一个钩子函数,可能包含如 mail_bot 模块中用于用户与机器人私下交互的逻辑。
class Channel(models.Model):
_name = 'discuss.channel'
_inherit = ['mail.thread']
# 1. 路由:/mail/message/post
mail_message_post() -> if "partner_emails" in post_data 创建联系人
-> thread = env['discuss.channel'].search([("id", "=", thread_id)])
-> thread.message_post() # 发布消息
# 2. 发布消息流程
message_post()
-> new_message = self._message_create([msg_values]) # 存储消息
-> self._message_post_after_hook(new_message, msg_values) # 钩子,如在 mail_bot 模块中添加mail_bot 模块添加用户与 odoo 初始化机器人私下交互的逻辑 `self.env['mail.bot']._apply_logic(self, msg_vals)`
-> self._notify_thread(new_message) # 给相关收件人通过多种方式发送通知,如: _notify_thread_by_sms 或 by_web_push, by_inbox, by_email
- _notify_thread 消息发送流程
构建通知数据,并通过 bus._sendmany(bus_notifications)
使用 PostgreSQL 的 pg_notify 发送异步通知。分发线程 (ImDispatch) 通过 监听 ‘imbus’ 通道,接收到此通知。如果是聊天频道或群组频道,调用_notify_thread_by_web_push() 发送 Web 推送通知。
# discuss.channel 扩展 mail.thread, 通过发送消息到前端
def _notify_thread(self, message, msg_vals=False, **kwargs):
# 调用父类方法
rdata = super()._notify_thread(message, msg_vals=msg_vals, **kwargs)
message_format = message.message_format()[0]
# 更新消息格式以包含临时ID
if "temporary_id" in self.env.context:
message_format["temporary_id"] = self.env.context["temporary_id"]
# 生成通知数据
payload = {"id": self.id, "is_pinned": True,
"last_interest_dt": fields.Datetime.now()}
bus_notifications = [
(self, "discuss.channel/last_interest_dt_changed", payload),
(self, "discuss.channel/new_message",
{"id": self.id, "message": message_format}),
]
# 使用 PostgreSQL 的内置函数 pg_notify 发送异步通知, SELECT "pg_notify"('imbus', json_dump(list(channels)))
# 其他客户端在该数据库连接中使用 LISTEN 命令监听 'imbus' 通道时,它们会接收到这个通知
self.env["bus.bus"].sudo()._sendmany(bus_notifications)
# 如果是聊天频道或群组频道,给相关人员发送 Web 推送通知
if self.is_chat or self.channel_type == "group":
self._notify_thread_by_web_push(message, rdata, msg_vals, **kwargs)
return rdata
- ImDispatch 分发线程
作为线程运行,监听 ‘imbus’ 通道上的数据库通知, 当接收到通知时,将其转发给订阅了相应通道的 WebSockets。
class ImDispatch(threading.Thread):
'分发线程'
def loop(self):
_logger.info("Bus.loop listen imbus on db postgres")
with odoo.sql_db.db_connect('postgres').cursor() as cr, selectors.DefaultSelector() as sel:
cr.execute("listen imbus") # 监听
while not stop_event.is_set():
if sel.select(TIMEOUT):
conn.poll()
channels = []
while conn.notifies:
channels.extend(json.loads(conn.notifies.pop().payload))
# 将 postgres 通知转发给订阅了相应通道的 websockets"
websockets = set()
for channel in channels:
websockets.update(self._channels_to_ws.get(hashable(channel), []))
for websocket in websockets:
websocket.trigger_notification_dispatching()
- 推送 Web 通知: _notify_thread_by_web_push
def _notify_thread_by_web_push(self, message, recipients_data, msg_vals=False, **kwargs):
"""
为每个用户的提及和直接消息发送 web 通知。
:param message: 需要通知的`mail.message`记录
:param recipients_data: 收件人信息列表(基于res.partner记录)
:param msg_vals: 使用它来访问与`message`相关联的值, 通过避免访问数据库中的消息内容来减少数据库查询次数
"""
# 提取通知所需的伙伴ID
partner_ids = self._extract_partner_ids_for_notifications(message, msg_vals, recipients_data)
if not partner_ids:
return
# 前端 navigator.serviceWorker.register("/web/service-worker.js")
# 以超级用户权限获取伙伴设备, service_worker.js 执行 jsonrpc 注册设备 call_kw/mail.partner.device/register_devices
partner_devices_sudo = self.env['mail.partner.device'].sudo()
devices = partner_devices_sudo.search([('partner_id', 'in', partner_ids)])
if not devices:
return
# 准备推送负载
payload = self._notify_by_web_push_prepare_payload(message, msg_vals=msg_vals)
payload = self._truncate_payload(payload)
# 如果设备数量小于最大直接推送数量,则直接推送通知
if len(devices) < MAX_DIRECT_PUSH:
push_to_end_point()
else:
# 如果设备数量超过最大直接推送数量,则创建一个通知队列项并触发异步推送
self.env['mail.notification.web.push'].sudo().create([{...} for device in devices])
self.env.ref('mail.ir_cron_web_push_notification')._trigger()
8. chatbotService 处理用户与机器人对话时选择的答案
通过 MESSAGE_POST 事件监听器。如果当前步骤类型为 free_input_multi
,调用 debouncedProcessUserAnswer(message)
处理用户输入。否则,调用 _processUserAnswer(message)
, 保存答案到后端(通过 /chatbot/answer/save
RPC 调用)。调用 _triggerNextStep()
继续对话流程(参见标题5)。
this.bus.addEventListener("MESSAGE_POST", ({ detail: message }) => {
if (this.currentStep?.type === "free_input_multi") {
this.debouncedProcessUserAnswer(message)
} else {
this._processUserAnswer(message) -> this.rpc("/chatbot/answer/save") // 保存答案
->_triggerNextStep() // 发送用户答案到后端继续下一步,上面标题5
}
})
9. 前端接收及处理消息
- simpleNotificationService 初始化时启动 busService,从指定 URL 加载 websocket_worker 源码。
- websocket_worker 监听处理 message 事件,当接收到消息时触发 notificationBus 上的 notification 事件。
- 根据不同的消息类型分别处理,如当接收到新的消息时:添加到相应频道的消息列表中,触发
discuss.channel/new_message
事件。 - threadService 监听此事件,并调用 notifyMessageToUser(channel, message) 方法显示新消息。
simpleNotificationService -> busService.start()
-> startWorker()
// workerURL = http://localhost:8069/bus/websocket_worker_bundle?v=1.0.7"
-> worker = new workerClass(workerURL, {"websocket_worker"});
-> worker.addEventListener("message", handleMessage)
-> handleMessage = () => notificationBus.trigger(type, payload)
-> this.busService.addEventListener("notification", () => this._handleNotificationNewMessage(notify))
-> channel.messages.push(message);
-> this.env.bus.trigger("discuss.channel/new_message")
-> this.env.bus.addEventListener("discuss.channel/new_message", () => this.threadService.notifyMessageToUser(channel, message))
-> this.store.ChatWindow.insert({ thread });
消息数据 messageEv.data 内容示例:
[{
"type": "discuss.channel.member/seen",
"payload": {
"channel_id": 35,
"last_message_id": 658,
"guest_id": 9
},
},
{
"type": "discuss.channel/last_interest_dt_changed",
"payload": {
"id": 35,
"is_pinned": true,
"last_interest_dt": "2024-04-23 16:24:32"
},
},
{
"type": "discuss.channel/new_message",
"payload": {
"message": {
"body": "<p>hello!</p>",
"date": "2024-04-23 16:24:32",
"email_from": false,
"message_type": "comment",
}
}
}]
总结
本文深入剖析了 Odoo 在线客服功能背后的前后端消息通信的主要过程及细节。前端通过智能识别用户输入,区分命令与常规消息,发送消息到后端。MESSAGE_POST
事件触发 chatbotService
实时响应用户对机器人问题的选择。后端接收到消息后,存储、处理、转发并以多种方式通知相关收件人。前端通过 WebSocket 监听后端消息并展示。
相关功能介绍见:Odoo讨论+聊天模块