深入解析 Odoo 在线客服模块 (im_livechat)

2024-04-24 12:36

本文主要是介绍深入解析 Odoo 在线客服模块 (im_livechat),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

深入解析 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. 前端客服相关核心服务详解

在前端主要依赖以下三个核心服务:

  1. mail.thread: import(“@mail/core/common/thread_service”).ThreadService
  2. im_livechat.livechat: import(“@im_livechat/embed/common/livechat_service”).LivechatService
  3. 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.optionsLivechatService.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]# 更新消息格式以包含临时IDif "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`相关联的值, 通过避免访问数据库中的消息内容来减少数据库查询次数"""# 提取通知所需的伙伴IDpartner_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_devicespartner_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讨论+聊天模块


这篇关于深入解析 Odoo 在线客服模块 (im_livechat)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/931772

相关文章

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

Python 基于http.server模块实现简单http服务的代码举例

《Python基于http.server模块实现简单http服务的代码举例》Pythonhttp.server模块通过继承BaseHTTPRequestHandler处理HTTP请求,使用Threa... 目录测试环境代码实现相关介绍模块简介类及相关函数简介参考链接测试环境win11专业版python

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Java Scanner类解析与实战教程

《JavaScanner类解析与实战教程》JavaScanner类(java.util包)是文本输入解析工具,支持基本类型和字符串读取,基于Readable接口与正则分隔符实现,适用于控制台、文件输... 目录一、核心设计与工作原理1.底层依赖2.解析机制A.核心逻辑基于分隔符(delimiter)和模式匹

Java+AI驱动实现PDF文件数据提取与解析

《Java+AI驱动实现PDF文件数据提取与解析》本文将和大家分享一套基于AI的体检报告智能评估方案,详细介绍从PDF上传、内容提取到AI分析、数据存储的全流程自动化实现方法,感兴趣的可以了解下... 目录一、核心流程:从上传到评估的完整链路二、第一步:解析 PDF,提取体检报告内容1. 引入依赖2. 封装

深度解析Python yfinance的核心功能和高级用法

《深度解析Pythonyfinance的核心功能和高级用法》yfinance是一个功能强大且易于使用的Python库,用于从YahooFinance获取金融数据,本教程将深入探讨yfinance的核... 目录yfinance 深度解析教程 (python)1. 简介与安装1.1 什么是 yfinance?

Nginx添加内置模块过程

《Nginx添加内置模块过程》文章指导如何检查并添加Nginx的with-http_gzip_static模块:确认该模块未默认安装后,需下载同版本源码重新编译,备份替换原有二进制文件,最后重启服务验... 目录1、查看Nginx已编辑的模块2、Nginx官网查看内置模块3、停止Nginx服务4、Nginx

99%的人都选错了! 路由器WiFi双频合一还是分开好的专业解析与适用场景探讨

《99%的人都选错了!路由器WiFi双频合一还是分开好的专业解析与适用场景探讨》关于双频路由器的“双频合一”与“分开使用”两种模式,用户往往存在诸多疑问,本文将从多个维度深入探讨这两种模式的优缺点,... 在如今“没有WiFi就等于与世隔绝”的时代,越来越多家庭、办公室都开始配置双频无线路由器。但你有没有注