Skip to content

接入你的 App ​

除了网站,你也可以在自己的 App 里接待用户。做法是在 App 里放一个"联系客服"按钮,点击后打开 SealReply 托管的全屏聊天页。不用装 SDK,iOS、安卓、Flutter、React Native 都能用。消息、翻译、聊天前表单、工作时间、外观这些设置都和网站上的插件一样,客服还是在工作台里回复。

1. 开启聊天页,复制链接 ​

  1. 先按 安装聊天插件 完成域名验证。没验证的站点不能开启。

  2. 打开 站点 → 你的站点 → 安装,在 在 App 里使用 卡片里点 开启 App 聊天页。

  3. 复制聊天页链接,格式是:

    text
    https://chat.sealreply.com/sk_…

    sk_… 是站点的 key,和网站嵌入代码里的 data-site-key 一样。

这个开关默认是关的。没开启时打开链接只会看到"客服暂时无法打开",所以别人拿到你的站点 key,也不能用它冒充你的客服页。

2. 在 App 里打开 ​

推荐用系统内置浏览器打开,也就是 iOS 的 SFSafariViewController、安卓的 Custom Tabs、Flutter 的 inAppBrowserView。发图片、发文件、发语音都不用额外处理,用户看完点"完成"或返回就回到 App。

swift
import SafariServices

let url = URL(string: "https://chat.sealreply.com/sk_…")!
present(SFSafariViewController(url: url), animated: true)
kotlin
// build.gradle: implementation("androidx.browser:browser:1.8.0")
import androidx.browser.customtabs.CustomTabsIntent

CustomTabsIntent.Builder().build()
    .launchUrl(context, Uri.parse("https://chat.sealreply.com/sk_…"))
dart
// pubspec.yaml: url_launcher: ^6.3.0
import 'package:url_launcher/url_launcher.dart';

await launchUrl(
  Uri.parse('https://chat.sealreply.com/sk_…'),
  mode: LaunchMode.inAppBrowserView,
);
jsx
// npm install react-native-webview
import { WebView } from "react-native-webview";

<WebView source={{ uri: "https://chat.sealreply.com/sk_…" }} />

聊天页上没有关闭按钮,用户用 App 自己的返回或关闭按钮退出。

在 WebView 里打开 ​

想让聊天页嵌在 App 自己的界面里(比如放在一个标签页里),可以用 WebView(iOS 的 WKWebView、安卓的 WebView、React Native 的 react-native-webview)。要注意:

  • 开启 JavaScript 和本地存储。安卓 WebView 要设置 settings.javaScriptEnabled = true 和 settings.domStorageEnabled = true,不然打不开,用户每次打开也会被当成新访客。
  • 发图片和文件:安卓 WebView 要实现 WebChromeClient.onShowFileChooser,否则点"+"没反应。iOS 和 react-native-webview 不用处理。
  • 发语音:App 要有麦克风权限(iOS 在 Info.plist 写 NSMicrophoneUsageDescription;安卓申请 RECORD_AUDIO,并在 WebChromeClient.onPermissionRequest 里同意)。不想处理的话,可以在 站点 → 访客功能 里关掉语音消息。

用网页做的 App(H5、套壳 App)直接跳转到聊天页链接就行。聊天页不能放进 iframe,这样别的网站没法把你的客服页套进他们的页面。

3. 让客服认出已登录的用户 ​

如果你的 App 有登录,可以像网站一样识别已登录用户:用户在客服那里显示为已验证,手机、电脑上都是同一个人,还能在聊天菜单里看到自己的历史对话。

  1. 在 站点 → 你的站点 → 访客身份验证 开启,把密钥保存在你的服务器上。

  2. App 向你的服务器要当前用户的 identity_hash。它就是用密钥对用户 ID 做 HMAC-SHA256 的结果,和网站用的完全一样,算法见这里。

  3. 打开聊天页时,把这些参数加在链接的 # 后面:

    text
    https://chat.sealreply.com/sk_…#external_id=42&identity_hash=<签名>&name=张三&email=zhang%40example.com

    每个值都要做网址编码。下面的代码会自动编码:

swift
var parts = URLComponents()
parts.queryItems = [
  URLQueryItem(name: "external_id", value: user.id),
  URLQueryItem(name: "identity_hash", value: identityHash), // 你的服务器算的
  URLQueryItem(name: "name", value: user.name),
  URLQueryItem(name: "email", value: user.email),
]
let url = URL(string: "https://chat.sealreply.com/sk_…#" + (parts.percentEncodedQuery ?? ""))!
present(SFSafariViewController(url: url), animated: true)
kotlin
val fragment = Uri.Builder()
    .appendQueryParameter("external_id", user.id)
    .appendQueryParameter("identity_hash", identityHash) // 你的服务器算的
    .appendQueryParameter("name", user.name)
    .appendQueryParameter("email", user.email)
    .build().encodedQuery
CustomTabsIntent.Builder().build()
    .launchUrl(context, Uri.parse("https://chat.sealreply.com/sk_…#$fragment"))
dart
final fragment = Uri(queryParameters: {
  'external_id': user.id,
  'identity_hash': identityHash, // 你的服务器算的
  'name': user.name,
  'email': user.email,
}).query;
await launchUrl(
  Uri.parse('https://chat.sealreply.com/sk_…#$fragment'),
  mode: LaunchMode.inAppBrowserView,
);
jsx
const fragment = Object.entries({
  external_id: user.id,
  identity_hash: identityHash, // 你的服务器算的
  name: user.name,
  email: user.email,
}).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");

<WebView source={{ uri: `https://chat.sealreply.com/sk_…#${fragment}` }} />

用户退出登录、或者换了账号以后,下次打开聊天页时不带参数(或者带新用户的参数)就行。聊天页会自动换成新的访客,上一个用户的对话不会出现。

签名的安全

identity_hash 就像这个用户的聊天密码,拿到它的人能看到他的聊天记录。密钥只能放在服务器上,不要放进 App。

带签名的链接不会过期,谁拿到都能以这个用户的身份聊天。每次打开时现拼链接,不要写进日志、不要分享给别人。

# 后面的内容不会发给任何服务器(包括我们的),聊天页读到以后马上就从地址栏里去掉了(这个页面标签里刷新时仍认得是同一个用户)。不过用安卓 Custom Tabs 打开时,Chrome 可能会把完整链接记进这台手机的浏览记录。介意的话,改用下面的 WebView 注入方式,签名就不经过链接了。

不把签名放进链接(WebView) ​

用 WebView 打开时,可以在页面加载之前注入 window.sealreplySettings,写法和网站上一样。链接里就不用带 external_id 和 identity_hash。如果链接里也带了,以链接里的为准。

swift
let settings = """
window.sealreplySettings = { identity: {
  external_id: \(jsonString(user.id)),
  identity_hash: \(jsonString(identityHash)),
  name: \(jsonString(user.name))
} };
"""
let config = WKWebViewConfiguration()
config.userContentController.addUserScript(
  WKUserScript(source: settings, injectionTime: .atDocumentStart, forMainFrameOnly: true)
)
let webView = WKWebView(frame: .zero, configuration: config)
webView.load(URLRequest(url: URL(string: "https://chat.sealreply.com/sk_…")!))
kotlin
// build.gradle: implementation("androidx.webkit:webkit:1.12.0")
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import org.json.JSONObject

val identity = JSONObject()
    .put("external_id", user.id)
    .put("identity_hash", identityHash)
    .put("name", user.name)
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
    WebViewCompat.addDocumentStartJavaScript(
        webView,
        "window.sealreplySettings = { identity: $identity };",
        setOf("https://chat.sealreply.com"),
    )
}
webView.loadUrl("https://chat.sealreply.com/sk_…")
jsx
const settings = `window.sealreplySettings = { identity: ${JSON.stringify({
  external_id: user.id,
  identity_hash: identityHash,
  name: user.name,
})} }; true;`;

<WebView
  source={{ uri: "https://chat.sealreply.com/sk_…" }}
  injectedJavaScriptBeforeContentLoaded={settings}
/>

jsonString 指把字符串转成 JSON 字符串(带引号、转义),可以用 JSONEncoder 实现。不要直接把用户名拼进代码。

4. 告诉客服用户从哪个页面来 ​

网站上的插件会把访客当前的网址告诉客服。App 没有网址,可以在 # 后面加:

参数说明
page_title页面名称,比如 订单 #1024、商品:蓝色外套。不填时客服看到的是 App。
page_url这个页面在网站上对应的地址(必须以 http:// 或 https:// 开头),客服可以点开查看。没有就不填。
text
https://chat.sealreply.com/sk_…#page_title=订单%20%231024&page_url=https%3A%2F%2Fshop.example.com%2Forders%2F1024

全部参数 ​

都放在链接的 # 后面,用 & 连接,每个值都要做网址编码。全部是可选的。

参数说明
external_id用户在你系统里的 ID,最多 128 个字符
identity_hash用户 ID 的签名,和 external_id 一起才有效
name、email显示给客服,资料里已有的不会被覆盖
attributes、attributes_hash会员信息:attributes 是 JSON 对象(例如 {"plan":"金卡"}),签名方法见传入会员信息
page_title、page_url用户从哪个页面来,见上一节

聊天页会跟随手机的系统语言显示,深色模式也会跟随系统(如果你在外观里设成"跟随访客系统")。

用户离开 App 后,客服回复了怎么办 ​

聊天页只在打开时实时收消息。用户关掉以后客服才回复的话,可以这样提醒用户:

  • 邮件:客服回复慢的时候,聊天页会请用户留邮箱(见不在线时怎么接住客户)。已登录、传了 email 的用户不用再填。
  • 你自己的推送:订阅 Webhook 的 message.created。收到客服发的消息(sender.type 是 agent)时,用事件里的 conversation.visitor_id 调 GET /v1/visitors/{visitor_id}(API Key 需要 visitors:read 权限)拿到 external_id,也就是你的用户 ID,再用你 App 自己的推送通知他。用户点通知后再打开聊天页,就能看到回复。

常见问题 ​

现象怎么办
打开后显示"客服暂时无法打开"检查 在 App 里使用 是否已开启、站点的域名是否已验证、站点有没有因为超出套餐被暂停、链接里的 sk_… 是否完整。
用户每次打开都是新访客WebView 没开本地存储(安卓 domStorageEnabled),或者每次都用了无痕模式。
登录用户没有显示"已验证"签名不对。确认用的是这个站点的密钥、签名的是和 external_id 一模一样的字符串、链接里的值做了网址编码。
点"+"选不了图片安卓 WebView 要实现 onShowFileChooser,或者改用 Custom Tabs。

App 里的对话和网站上的一样计入这个站点的每月会话数,在工作台里也和网站访客放在一起。