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 位小寫十六進位。 - 簽的就是
externalId本身,別的都不簽,而且一定要一起傳:WebView 裡的元件永遠會帶externalId(你不傳它會自己產生匿名的),所以只簽email驗證不了任何人,訪客只會停在「未驗證」。 secretKey是你收件匣的密鑰(一個收件匣一個),整串、包含sk_前綴。它只在建立收件匣或輪換密鑰時顯示一次(該收件匣那一列的 ⋮ 選單 → 輪換密鑰)。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 方案上線更快、且永遠全功能。