App 内聊天(WebView)
用 WebView 把完整客服挂件嵌入任意 App , 全功能、自动更新、一次接入。
在原生 App 里加客服,最快的方式是用 WebView 加载我们托管的全屏挂件页。你会得到与网页版完全一致的挂件:首页、消息、帮助文档、搜索、表情、附件、时间戳、已读、结束会话,而且随平台自动更新。一次接入之后,App 无需再为客服功能改代码、发新版。
快速开始
让 WebView 加载这个地址。用户已登录时,必须带上 externalId 和 email(最好再加 name),否则客服后台永远只看到匿名访客。每个参数值都要 URL 编码(@→%40、空格→%20、中文名也要编码):
https://<你的域名>/api/widget-embed?appId=YOUR_APP_ID&externalId=USER_ID&email=USER_EMAIL&name=USER_NAME只有用户未登录时,才退回匿名形式:
https://<你的域名>/api/widget-embed?appId=YOUR_APP_ID⚠️ 后台显示「匿名访客」、没有邮箱?这是最常见的接入错误:你的 WebView 地址里 漏了 email/externalId。系统不可能凭空知道用户是谁,必须 App 主动传。 若访客 ID 长得像 anon-…,就说明什么都没传(那是挂件自动生成的匿名号)。
JavaScript / 任意 WebView(非 Flutter)
Electron、桌面壳、React Native、或你自己设 URL 的原生 WKWebView , 用 JS 拼地址即可。URLSearchParams 会自动帮你做 URL 编码,不用手动转义:
// 打开客服 WebView 前,用当前登录用户拼出地址。
function buildSupportUrl(domain, user) {
const params = new URLSearchParams({
appId: 'YOUR_APP_ID',
platform: 'windows', // 强烈建议:决定这个访客能看到哪些帮助文档
// ios / macos_appstore / macos / android / windows / web
// locale: userSelectedLanguage, // 可选:App 有语言开关时才传,并加 forceLocale: '1'
// 不传 = 跟随设备语言(覆盖 App Store 的 50 种本地化)
// —— 登录用户务必传这几项,否则后台是「匿名访客」——
externalId: user.id, // 你系统里的用户唯一 ID
email: user.email, // 用户邮箱(关键)
name: user.name || '', // 用户名(可选)
// hmac: user.hmac, // 可选:由你服务端算,防止冒充(见下方 HMAC)
// attrs: JSON.stringify({ 套餐: user.plan, 到期日: user.expireAt }), // 可选:自定义资料
});
return `https://${domain}/api/widget-embed?${params.toString()}`;
}
// 用法:把返回的 url 交给 WebView 加载(替换掉原来只有 ?appId=... 的地址)。
const url = buildSupportUrl('askais.com', currentUser);
myWebView.loadURL(url); // Electron: win.loadURL(url);原生:注入该 URLFlutter(webview_flutter)
在 pubspec.yaml 加 webview_flutter: ^4.x,然后:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class SupportPage extends StatefulWidget {
const SupportPage({super.key});
@override
State<SupportPage> createState() => _SupportPageState();
}
class _SupportPageState extends State<SupportPage> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
// 登录用户务必带上 externalId + email,否则后台是「匿名访客」(没有姓名/邮箱)。
// 未登录时才只传 appId + locale。Uri.https 会自动做 URL 编码。
final uri = Uri.https('askais.com', '/api/widget-embed', {
'appId': 'YOUR_APP_ID',
'platform': Platform.isIOS ? 'ios' : 'android', // 强烈建议:决定可见的帮助文档
// 不传 locale = 跟随设备语言(已覆盖 App Store 的 50 种本地化)。
// App 自己有语言开关时才传,并同时加 'forceLocale': '1':
// 'locale': appSettings.selectedLanguage,
'externalId': user.id, // 你系统里的用户唯一 ID —— 关键
'email': user.email, // 用户邮箱 —— 关键
'name': user.name, // 可选
// 'hmac': hmacFromYourServer, // 可选:由你服务端算,防冒充
});
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..loadRequest(uri);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Support')),
// Full-screen: drop the appBar and use your own close button.
body: SafeArea(child: WebViewWidget(controller: _controller)),
);
}
}
// Open it from wherever your "Support" button lives:
// Navigator.push(context,
// MaterialPageRoute(builder: (_) => const SupportPage()));其它技术栈的 WebView(Swift WKWebView、Kotlin WebView、React Native react-native-webview)做法完全一样:加载地址、开启 JavaScript 即可。
原生 iOS 与 Android
没有 SDK 要装 , 同样只是拼出这个地址交给 WebView。注意 Mac:上架版与 DMG 版 是同一个操作系统,所以那个值必须来自编译期 flag,不能靠运行期判断。
// ── iOS (Swift / WKWebView) ────────────────────────────────
// Mac Catalyst / macOS 版把 platform 换成 macos_appstore 或 macos。
#if targetEnvironment(macCatalyst)
let platform = "macos_appstore" // 上架版;官网 DMG 版传 "macos"
#else
let platform = "ios" // iPhone 与 iPad 同属 ios
#endif
var comps = URLComponents(string: "https://\(domain)/api/widget-embed")!
comps.queryItems = [
.init(name: "appId", value: "YOUR_APP_ID"),
.init(name: "platform", value: platform),
.init(name: "externalId", value: user.id), // 登录用户务必传
.init(name: "email", value: user.email), // 登录用户务必传
.init(name: "name", value: user.name),
// App 自己有语言开关时才传这两项:
// .init(name: "locale", value: settings.language),
// .init(name: "forceLocale", value: "1"),
]
webView.load(URLRequest(url: comps.url!)) // URLComponents 自动做 URL 编码
// ── Android (Kotlin / WebView) ─────────────────────────────
val url = Uri.parse("https://$domain/api/widget-embed")
.buildUpon()
.appendQueryParameter("appId", "YOUR_APP_ID")
.appendQueryParameter("platform", "android")
.appendQueryParameter("externalId", user.id)
.appendQueryParameter("email", user.email)
.appendQueryParameter("name", user.name)
// .appendQueryParameter("locale", settings.language)
// .appendQueryParameter("forceLocale", "1")
.build()
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true // 挂件用 localStorage 记住访客
webView.loadUrl(url.toString())URL 参数
appId(必填) , 你收件箱的 App ID。locale, 界面语言。绝大多数情况不要传 , 不传时挂件跟随 WebView 上报的设备语言,这也是用户的预期。界面文案已覆盖 App Store Connect 的全部 50 种本地化,清单之外的语言回退英文。 只有当你的 App 自己有语言开关时才传 , 传用户选的那个,并同时带上forceLocale=1让它盖过设备语言。绝对不要写死:写死zh-Hans正是日语 App 里出现中文挂件的原因。platform(强烈建议传):访客用的是哪个构建。 它决定这个访客能看到哪些帮助文档,以及 AI 回复受不受 App Store 约束。不传的话只能从 WebView 的 User-Agent 猜:认得出 iPhone、Android、Windows,但分不出 Mac App Store 版和 DMG 版,也认不出桌面模式的 iPad。取值:ios(iPhone 与 iPad)、macos_appstore(Mac App Store 上架版)、macos(官网 DMG 直装版)、android、windows、web。externalId, 你系统里的用户唯一 ID。传了客服后台就能认出用户、合并其历史会话。email、name、avatarUrl, 展示给客服的资料。hmac, 身份签名(见下)。可选。
按平台隐藏内容(App Store 合规)
App Store 3.1.1 不允许 App 内展示站外购买、订阅与返佣内容。因此每一篇帮助文档、每一条 FAQ 都可以单独设置在哪些平台隐藏:在后台的「帮助中心」或「FAQ」页面,点亮那一条下面的平台标签即可。被隐藏后,该平台的访客在列表里看不到它,用直链也打不开。
AI 回答时不读帮助文档和 FAQ,用的是「训练」里的内容。那里的文件和问答也有同样的平台标签,对某个平台隐藏的条目,不会用在该平台访客的回答里。如果连 AI 也不该提到,两边都要隐藏。
能不能可靠生效取决于两件事。第一,App 应该自己传 platform,从 User-Agent 猜只是后备。第二,传的值要反映构建类型,不能只看操作系统:
- iPad 不单列。iPad 跑的是同一个 iOS App、受同一套规则约束, 所以
ios已经涵盖 iPhone 与 iPad。 - Mac 有两个构建。Mac App Store 上架版受 3.1.1 约束, 官网直接下载的 DMG 版不受。两者是同一个操作系统,运行期分辨不出来 , 要在构建时决定(例如用 Xcode 的编译期 flag),分别传
macos_appstore与macos。传错的后果:上架版会在审核时 露出订阅内容,或者 DMG 版白白少给用户一半的说明文档。
改动即时生效 , 这个设置存在后台、不在 App 里,所以调整可见范围永远不需要发版。
身份签名(HMAC,可选)
为证明某个 externalId 确实是你们已登录的用户(防止冒充),可附带 HMAC 签名。签名必须在你们的服务端计算 , 密钥绝不能进 App。
- 算法:
hmac = HMAC-SHA256(secretKey, externalId),输出 64 位小写十六进制。 - payload 就是
externalId,所以一定要一起传:不传的话挂件会用自己的匿名 id,只签email的签名会被拒绝。 secretKey是你收件箱的密钥(一个收件箱一个,在收件箱设置里取)。不传hmac则该访客视为「未验证身份」。
import { createHmac } from 'node:crypto';
// SECRET_KEY lives only on your server, never ship it inside the app.
const hmac = createHmac('sha256', SECRET_KEY)
.update(externalId) // exactly the externalId you pass to the widget
.digest('hex');
// Return { externalId, email, hmac } to the app, which appends them to the URL.平台配置
- Android:保留
INTERNET权限;拦截物理返回键,先让 WebView 返回上一屏。 新版webview_flutter支持网页里的<input type="file">发附件。 - iOS:走 HTTPS,无需 ATS 例外。图片上传是挂件自带功能,
WKWebView会自动弹出系统相册选择器,原生端不用写任何代码。从相册选图无需任何权限;只有想支持「拍照」时,才在Info.plist加一行NSCameraUsageDescription(相机用途说明),否则用户点拍照会闪退。
想要纯原生 UI?
如果你需要自己手写原生聊天界面(而非 WebView),用 Mobile SDKs 页里那套「仅逻辑」客户端。 对大多数 App 来说,上面的 WebView 方案上线更快、且永远全功能。