指南

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);原生:注入该 URL

Flutter(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 方案上線更快、且永遠全功能。