# 客服功能说明

引擎修改要求:请按目标平台完成以下修改。CEF 后端需要解除文件选择框拦截,否则客服附件上传不可用;鸿蒙平台需要完成浏览器绑定及状态同步修复,否则可能在显示隐藏时崩溃或黑屏。修改后需要重新生成目标平台工程并打包;涉及 C++ 的修改还需要重新编译。

# CEFWebBrowserWindow.cpp(CEF 后端)

FCEFWebBrowserWindow::OnFileDialog()

文件:CEFWebBrowserWindow.cpp;修改函数:FCEFWebBrowserWindow::OnFileDialog()。

引擎相对路径:

Engine/Source/Runtime/WebBrowser/Private/CEF/CEFWebBrowserWindow.cpp
1

搜索 FCEFWebBrowserWindow::OnFileDialog()。当前使用的 UE 5.6.1 源码引擎中,函数从第 1416 行开始,修改位置为第 1425~1427 行;其他引擎版本应按函数名定位,保留其原有函数签名。

修改后(函数体):

/* 删除代码:解除文件选择框拦截。
UE_LOG(LogWebBrowser, Error, TEXT("FileDialogs are prevented."));
return true;
*/

// 新增代码:交给 CEF 默认处理文件选择对话框。
return false;
1
2
3
4
5
6
7

目标引擎若仍有上述拦截实现,需要修改引擎源码并重新编译,仅替换 SDK 插件无法使此补丁生效。

# 鸿蒙平台引擎修改

以下五处修改用于修复当前 UE 5.6.1 鸿蒙源码引擎中的浏览器显示隐藏问题。其他引擎版本请按文件和函数名检查;已包含对应修复时无需重复修改。以下路径均相对于引擎根目录。

鸿蒙平台需要同时完成函数绑定、状态同步和组件标识修改。仅补充空指针检查能够避免该处崩溃,但不能解决浏览器黑屏。

# AkiJSBind.ets.template

# AkiJSBind.MainThreadBindAki()

文件:AkiJSBind.ets.template;修改函数:AkiJSBind.MainThreadBindAki()。

引擎相对路径:

Engine/Build/OpenHarmony/entry/src/main/ets/common/AkiJSBind.ets.template
1

定位 MainThreadBindAki() 的 WebView 绑定区域,在已有注册后增加 WebView.setVisible。以下为局部绑定代码,保留函数中的其他注册。

修改后:

nativeRender.JSBind.bindFunction("WebView.reload", WebView.reload);

// 新增代码:注册浏览器可见性函数。
nativeRender.JSBind.bindFunction("WebView.setVisible", WebView.setVisible);
1
2
3
4

WebView.setVisible 的实现已存在,此处补齐 C++ 调用所需的绑定。

# OpenHarmonyArkTSWebBrowser.cpp

# FArkTSOpenHarmonyWebBrowser::SetVisibility()

文件:OpenHarmonyArkTSWebBrowser.cpp;修改函数:FArkTSOpenHarmonyWebBrowser::SetVisibility()。

引擎相对路径:

Engine/Source/Runtime/WebBrowser/Private/OpenHarmony/OpenHarmonyArkTSWebBrowser.cpp
1

定位 FArkTSOpenHarmonyWebBrowser::SetVisibility()。

修改后:

void FArkTSOpenHarmonyWebBrowser::SetVisibility(bool InIsVisible)
{
    auto jsFunc = aki::JSBind::GetJSFunction("WebView.setVisible");

    // 新增代码:绑定缺失时安全返回,避免空指针崩溃。
    if (jsFunc == nullptr)
    {
        UE_LOG(LogTemp, Error, TEXT("鸿蒙浏览器未注册 WebView.setVisible,无法更新可见性。"));
        return;
    }
    jsFunc->Invoke<void>(WebViewTag, InIsVisible);
}
1
2
3
4
5
6
7
8
9
10
11
12

绑定缺失时记录错误并返回,避免解引用空指针。该错误日志仅在异常情况下输出。

# WebView.ts

文件:WebView.ts;修改位置:WebViewInfo 类声明、copyWebViewInfo() 辅助函数,以及 WebView 类中的 setWebViewRect()、setVisible()、setOpacityWebView()、setBackgroundTransparent()、setScalesPageToFit() 五个函数。

引擎相对路径:

Engine/Build/OpenHarmony/entry/src/main/ets/webview/WebView.ts
1

# WebViewInfo 类声明

增加 @Observed,以下仅展示修改后的类声明开头,类中原有字段和构造函数保持不变。

修改后:

// 新增代码:将浏览器属性变化同步到持有该对象的界面组件。
@Observed
export class WebViewInfo {
1
2
3

# copyWebViewInfo()

删除整个辅助函数。

修改后:

/* 删除代码:属性改为原地更新,不再需要复制浏览器状态对象。
function copyWebViewInfo(viewInfo: WebViewInfo): WebViewInfo {
    let newViewInfo: WebViewInfo = new WebViewInfo(viewInfo.x, viewInfo.y, viewInfo.w, viewInfo.h, viewInfo.viewTag);
    newViewInfo.url = viewInfo.url;
    newViewInfo.isUrl = viewInfo.isUrl;
    newViewInfo.zoomAccess = viewInfo.zoomAccess;
    newViewInfo.visible = viewInfo.visible;
    newViewInfo.controller = viewInfo.controller;
    newViewInfo.opacity = viewInfo.opacity;
    newViewInfo.backgroundColor = viewInfo.backgroundColor;
    return newViewInfo;
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13

# WebView.setWebViewRect()

删除对象复制和数组项替换,直接更新原对象的区域属性。以下为修改后的完整函数。

修改后:

static setWebViewRect(viewTag: number, x: number, y: number, w: number, h: number): void {
    let index = globalThis.webViewIndexMap.get(viewTag);
    let tmpWebInfo: WebViewInfo = globalThis.webViewArray[index];
    tmpWebInfo.x = px2vp(x);
    tmpWebInfo.y = px2vp(y);
    tmpWebInfo.w = px2vp(w);
    tmpWebInfo.h = px2vp(h);

    /* 删除代码:直接更新原对象,不再复制并替换数组项。
    let newViewInfo: WebViewInfo = copyWebViewInfo(tmpWebInfo);
    globalThis.webViewArray[index] = newViewInfo;
    */
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# WebView.setVisible()

删除对象复制和数组项替换,直接更新原对象的可见性。以下为修改后的完整函数。

修改后:

static setVisible(viewTag: number, visible: boolean): void {
    let index = globalThis.webViewIndexMap.get(viewTag);
    if (globalThis.webViewArray[index].visible == visible) {
        return;
    }
    let tmpWebInfo: WebViewInfo = globalThis.webViewArray[index];
    tmpWebInfo.visible = visible;

    /* 删除代码:直接更新原对象,不再复制并替换数组项。
    let newViewInfo: WebViewInfo = copyWebViewInfo(tmpWebInfo);
    globalThis.webViewArray[index] = newViewInfo;
    */
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# WebView.setOpacityWebView()

删除对象复制和数组项替换,直接更新原对象的透明度。以下为修改后的完整函数。

修改后:

static setOpacityWebView(viewTag: number, opacity: number): void {
    let index = globalThis.webViewIndexMap.get(viewTag);
    if (globalThis.webViewArray[index].opacity == opacity) {
        return;
    }
    let tmpWebInfo: WebViewInfo = globalThis.webViewArray[index];
    tmpWebInfo.opacity = opacity;

    /* 删除代码:直接更新原对象,不再复制并替换数组项。
    let newViewInfo: WebViewInfo = copyWebViewInfo(tmpWebInfo);
    globalThis.webViewArray[index] = newViewInfo;
    */
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# WebView.setBackgroundTransparent()

删除对象复制和数组项替换,直接更新原对象的背景色。以下为修改后的完整函数。

修改后:

static setBackgroundTransparent(viewTag: number): void {
    let index = globalThis.webViewIndexMap.get(viewTag);
    if (globalThis.webViewArray[index].backgroundColor == Color.Transparent) {
        return;
    }
    globalThis.webViewArray[index].backgroundColor = Color.Transparent;

    /* 删除代码:直接更新原对象,不再复制并替换数组项。
    let newViewInfo: WebViewInfo = copyWebViewInfo(globalThis.webViewArray[index]);
    globalThis.webViewArray[index] = newViewInfo;
    */
}
1
2
3
4
5
6
7
8
9
10
11
12

# WebView.setScalesPageToFit()

删除对象复制和数组项替换,直接更新原对象的缩放设置。以下为修改后的完整函数。

修改后:

static setScalesPageToFit(viewTag: number, scalesPageToFit: boolean): void {
    let index = globalThis.webViewIndexMap.get(viewTag);
    if (globalThis.webViewArray[index].zoomAccess == scalesPageToFit) {
        return;
    }
    let tmpWebInfo: WebViewInfo = globalThis.webViewArray[index];
    tmpWebInfo.zoomAccess = scalesPageToFit;

    /* 删除代码:直接更新原对象,不再复制并替换数组项。
    let newViewInfo: WebViewInfo = copyWebViewInfo(tmpWebInfo);
    globalThis.webViewArray[index] = newViewInfo;
    */
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这些方法直接更新同一个状态对象,不再通过复制对象刷新界面,也不重新创建浏览器控制器。

# UeWebview.ets

# UeWebView.viewInfo 属性声明

文件:UeWebview.ets;修改声明:UeWebView 组件的 viewInfo 属性。

引擎相对路径:

Engine/Build/OpenHarmony/entry/src/main/ets/webview/UeWebview.ets
1

定位 UeWebView 中的 viewInfo 声明。

修改后:

/* 删除代码:普通成员未建立浏览器状态同步关系。
viewInfo: WebViewInfo = new WebViewInfo(0, 0, 0, 0, 0);
*/

// 新增代码:同步父组件传入的浏览器状态。
@ObjectLink viewInfo: WebViewInfo;
1
2
3
4
5
6

不要保留本地初始化。该属性由父组件传入,与前一步的 @Observed 配合,使尺寸、可见性等属性更新到界面。

# Index.ets.template

# Index.build()

文件:Index.ets.template;修改函数:Index.build(),具体为渲染 webViewArray 的 ForEach。

引擎相对路径:

Engine/Build/OpenHarmony/entry/src/main/ets/pages/Index.ets.template
1

定位渲染 webViewArray 的 ForEach。以下为 Index.build() 中的局部代码,保留函数中的其他布局。

修改后:

ForEach(this.webViewArray, (item: WebViewInfo) => {
    UeWebView({ viewInfo: item })
},
/* 删除代码:位置和尺寸变化不应改变组件标识。
(item: WebViewInfo) => (item.viewTag + item.x + item.y + item.w + item.h).toString()
*/
// 新增代码:仅以固定的 viewTag 标识组件。
(item: WebViewInfo) => item.viewTag.toString())
1
2
3
4
5
6
7
8

位置和尺寸不再参与组件标识,避免布局变化时销毁并重新创建浏览器组件。

# 修改生效与验证

修改引擎源码及模板后,通过 UE 重新生成鸿蒙工程并打包,其中 C++ 修改需要重新编译。不要仅修改 Intermediate/OpenHarmony 内的生成文件;下次生成工程会覆盖这些文件。如果直接使用已有的 DevEco 工程打包,需要先将上述模板和源文件修改同步到该工程。

新包安装后,验证应用启动、客服首次显示、反复显示隐藏以及浏览器区域尺寸变化,确认没有崩溃、黑屏或因布局变化触发的重建。显示隐藏时按本页控制交互说明调用对应接口。

本节代码不包含临时诊断日志,不需要添加组件创建、销毁、布局或可见性打印回调。

# 功能介绍

从 CSDK 2.23.0 开始,可通过本页 UE 接口接入客服功能,包括查询客服开关、获取当前角色的客服地址、加载客服界面,以及获取客服会话标识。

客服是否开启可在初始化成功后查询;获取角色客服地址及有效客服会话需要角色登录成功。客服未读数量的查询、清除和变化通知复用会话接口。

# 为什么必须使用 UCsdkWebBrowser

客服界面必须使用 SDK 提供的 UCsdkWebBrowser 加载。 该控件提供客服接入所需的加载、交互控制、输入和附件选择能力,并统一提供加载状态事件。

使用该控件可获得 SDK 支持的完整客服能力。加载客服地址应调用 CsdkLoadURL(),交互控制使用本页列出的公开接口。

项目 内容
客服业务接口类 UCsdkCustomerServiceLibrary
业务接口模块/头文件 CsdkRuntime/CustomerService/CsdkCustomerServiceLibrary.h
浏览器控件类 UCsdkWebBrowser
浏览器模块/头文件 CsdkBrowser/Widget/WebBrowser/CsdkWebBrowser.h
客服业务接口分类 CSDK|CustomerService
浏览器接口分类 CSDK|WebBrowser

C++ 调用方在自身模块的 Build.cs 中添加依赖:

PrivateDependencyModuleNames.AddRange(new[] { "CsdkRuntime", "CsdkBrowser" });
1

如果自身模块的公开头文件暴露了这些模块的类型,应按 Unreal 模块规则配置公开依赖。

# 客服业务接口

# 查询客服是否开启

static bool IsCustomerServiceEnabled();
1

初始化成功后调用,返回产品是否开启客服。未初始化、初始化失败、服务端未开启或 SDK 已销毁时返回 false。该接口不要求角色登录;返回 true 不代表角色已登录或客服界面已加载完成。

# 获取客服会话标识

static FCsdkSessionId GetCustomerServiceSessionId();
1

返回当前已登录角色的固定客服会话。SDK 未初始化、角色未登录或角色标识无效时,返回无效会话。

# 获取客服地址

static FString GetCustomerServiceURL();
1

无参数,返回根据当前角色构建完成的完整客服 URL,可直接交给 UCsdkWebBrowser::CsdkLoadURL();接入方无需自行补充登录参数。

要求 SDK 初始化成功、角色已登录、客服已开启、角色及区服标识有效,且客服地址配置有效。无法获取时返回空字符串,调用方应先判断非空。该函数只返回地址,不自动创建控件或加载客服界面。

地址对应当前角色。登出或换号后应清空旧客服界面,待新角色登录后重新获取并加载地址。不要将地址长期缓存或写入日志。

# 客服属于会话

客服使用现有会话体系,同一已登录角色对应一个固定客服会话:

字段 值
ReceiverId "cs"
ServerId 当前角色的 ServerId
Type ECsdkSessionType::CustomerService

应使用 GetCustomerServiceSessionId() 获取该标识。客服未读数量查询、清除及变化通知复用会话接口,具体用法请跳转:

客服未读返回的是数量,可用于数字角标或以是否大于零判断红点。当前版本的未读数量查询返回类型为 int64;会话文档中的旧示例若仍标为 int32,以当前 SDK 声明为准。

客服未读计入总未读,不要求客服出现在本地会话列表。清除由业务显式调用现有会话清除接口;SDK 不因客服界面加载完成或显示在前台而自动清除。绑定变化通知后应主动查询一次当前数量。

# 浏览器公开接口

# 加载地址

void CsdkLoadURL(const FString& URL);
1
参数 说明
URL 待加载地址,客服场景传入 GetCustomerServiceURL() 返回的非空地址

加载指定地址,支持在控件创建前调用。传入空字符串加载 about:blank,可用于清空客服界面。每次调用都会发起加载,日常显示切换不应重复调用。

控件也支持创建时加载 InitialURL,为空时加载空白界面。编辑器设计预览不发起加载。

# 控制交互

void CsdkEnableBrowserInteraction();
void CsdkDisableBrowserInteraction();
1
2

默认禁止交互。启用后允许操作客服界面;禁用后停止接收交互,保留已加载内容。支持在控件创建前设置交互状态。

显示客服界面时调用 CsdkEnableBrowserInteraction(),隐藏时调用 CsdkDisableBrowserInteraction()。这两个接口不重新加载地址,也不修改外层 UMG 控件的可见性,控件显示隐藏由接入方管理。

# 加载及浏览器事件

事件 参数 说明
OnCsdkLoadStarted const FString& Url 开始加载
OnCsdkLoadCompleted const FString& Url 加载完成
OnCsdkLoadError int32 ErrorCode, const FString& ErrorMessage, const FString& Url 加载失败,返回错误信息

启用交互不代表加载完成,可通过加载事件更新界面的加载提示或错误提示。

# 控件使用说明

日常显示切换应保留同一个控件实例,避免通过销毁并重新创建控件实现切换。登出或换号时禁用交互并加载空白地址,随后按新角色重新获取并加载客服地址。

版本下载见修订说明。

本文档对解决你的问题有所帮助?