SupaTalker 文件
頻道宿主站台

程式碼整合

給網站開發人員:匿名嵌入的一行 script、會員認證嵌入的入場票與會員資料、小工具的語言,以及說明文章的自動翻譯

這一頁寫給負責網站的開發人員:從後台複製下來的嵌入碼與程式片段,要放在網站的哪裡、怎麼改。 網站的網址要先在宿主站台登記好,沒有登記的網站,放了嵌入碼也無法使用。

兩種嵌入,差在要不要後端

  • 匿名嵌入 —— 只要在網頁加一行 <script>,靜態網頁也可以
  • 會員認證嵌入 —— 網站後端要在會員登入後簽一張「入場票」,放進那一行 <script>。適合已經有會員登入與會員資料的網站

以下範例的 https://your-domain.example.com 代表 SupaTalker 後台的網址,Host ID 與 secret 也是示意值,一律以後台複製下來的為準。

匿名嵌入

在後台「匿名嵌入」區登記網址後,按「複製嵌入碼」,把這一行貼到每個要出現聊天小工具的頁面,放在 </body> 之前:

index.html
<!doctype html>
<html lang="zh-Hant">
  <body>
    <!-- 網站原本的內容 -->

    <script src="https://your-domain.example.com/embed.js"></script>
  </body>
</html>

頁面右下角就會出現氣泡按鈕,訪客點它打開聊天小工具。以下是 SupaTalker 官網實際嵌入的樣子:

嵌入後,網站右下角出現氣泡按鈕

點氣泡按鈕打開聊天小工具

氣泡的顏色、形狀、位置與名稱由後台的全域設定統一決定,不必在這一行加任何屬性;後台改了之後約 1 分鐘生效,不用重貼。

會員認證嵌入

適合已經有會員登入機制、用 Node、PHP、Python 等後端語言建立的網站。運作方式:

  1. 會員登入網站後,網站後端用 Host ID 與 secret 簽一張入場票
  2. 把入場票放進 <script> 的 data-member-token 屬性,輸出在頁面上
  3. 聊天小工具用這張票確認會員身分,訪客就以會員身分對話,對話紀錄也跟著會員保留

在後台「會員認證嵌入」區建立站台後,該站台的「複製 Node 片段」「複製 PHP 片段」「複製 Python 片段」會複製已經填好 Host ID 與 secret 的程式片段,內容如下:

Node.js
const crypto = require('node:crypto')
const appOrigin = "https://your-domain.example.com"
const hostId = "embed_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
const secret = "<secret>"
const base64url = (value) => Buffer.from(value).toString('base64url')
const now = Math.floor(Date.now() / 1000)
const payload = { v: 1, iss: hostId, sub: currentUser.id, jti: crypto.randomUUID(), name: currentUser.name, email: currentUser.email, iat: now, exp: now + 120 }
const signingInput = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })) + '.' + base64url(JSON.stringify(payload))
const token = signingInput + '.' + crypto.createHmac('sha256', secret).update(signingInput).digest('base64url')
res.setHeader('Cache-Control', 'private, no-store')
res.write('<script src="' + appOrigin + '/embed.js" data-member-token="' + token + '"><\/script>')
PHP
<?php
$appOrigin = "https://your-domain.example.com";
$hostId = "embed_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
$secret = "<secret>";
$base64url = fn($value) => rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
$now = time();
$payload = ['v' => 1, 'iss' => $hostId, 'sub' => $currentUser['id'], 'jti' => bin2hex(random_bytes(16)), 'name' => $currentUser['name'], 'email' => $currentUser['email'], 'iat' => $now, 'exp' => $now + 120];
$signingInput = $base64url(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])) . '.' . $base64url(json_encode($payload));
$token = $signingInput . '.' . $base64url(hash_hmac('sha256', $signingInput, $secret, true));
header('Cache-Control: private, no-store');
echo '<script src="' . htmlspecialchars($appOrigin, ENT_QUOTES) . '/embed.js" data-member-token="' . htmlspecialchars($token, ENT_QUOTES) . '"><\/script>';
Python
import base64, hashlib, hmac, json, time, uuid
app_origin = "https://your-domain.example.com"
host_id = "embed_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
secret = "<secret>"
def base64url(value): return base64.urlsafe_b64encode(value).rstrip(b'=').decode()
now = int(time.time())
payload = {'v': 1, 'iss': host_id, 'sub': current_user.id, 'jti': str(uuid.uuid4()), 'name': current_user.name, 'email': current_user.email, 'iat': now, 'exp': now + 120}
signing_input = base64url(json.dumps({'alg': 'HS256', 'typ': 'JWT'}, separators=(',', ':')).encode()) + '.' + base64url(json.dumps(payload, separators=(',', ':')).encode())
signature = base64url(hmac.new(secret.encode(), signing_input.encode(), hashlib.sha256).digest())
token = signing_input + '.' + signature
response.headers['Cache-Control'] = 'private, no-store'
response.write('<script src="' + app_origin + '/embed.js" data-member-token="' + token + '"><\/script>')

片段裡有幾個變數要換成網站自己的:

片段裡的名稱換成
currentUser(Python 為 current_user、PHP 為 $currentUser)目前登入的會員,見下一節
res/response網站框架的回應物件
res.write(...)/echo/response.write(...)改成把這段 <script> 輸出到頁面 </body> 之前,例如放進頁面樣板

secret 只能放在網站後端

secret 等於這個站台的鑰匙,不能出現在瀏覽器看得到的地方(前端 JavaScript、HTML 原始碼),也不要寫進版本控制。 建議改從環境變數讀取。secret 外洩時,到後台按「輪替 secret」換一把新的,再更新到網站。

如何注入會員資料

會員資料放在入場票的內容(上面片段的 payload)裡:

欄位必填說明
v✓固定為 1
iss✓後台顯示的 Host ID
sub✓會員在網站上的固定不變的 ID。以英文或數字開頭,只能用英數字與 . _ : -,最長 128 字
jti✓每張票不同的隨機值,片段已經產生好
iat、exp✓簽發與到期時間(Unix 秒)。到期最多只能在簽發後 120 秒
name選填會員姓名,最多 120 字
email選填會員 Email,最多 254 字
  • sub 決定「是不是同一位會員」:同一個 sub 視為同一位會員,對話紀錄都歸在同一位訪客底下。用資料庫裡的會員編號,不要用 Email(Email 含 @ 不符合格式,而且會員可能改 Email)
  • name、email 只會寫入一次:後台的訪客資料這兩欄還是空的時候才會填入,之後網站這邊改了,不會覆蓋後台已有的值
  • 每次載入頁面都要重簽:一張票只能用一次,而且 120 秒內就到期。所以這段 <script> 必須在每次產生頁面時當場簽發,不能被快取;片段裡的 Cache-Control: private, no-store 就是為此設定

以 Node.js(Express)為例,把片段整理成一個函式,在會員登入後的頁面輸出:

server.js
const crypto = require('node:crypto')

const appOrigin = 'https://your-domain.example.com'
const hostId = process.env.SUPATALKER_HOST_ID
const secret = process.env.SUPATALKER_EMBED_SECRET
const base64url = (value) => Buffer.from(value).toString('base64url')

function memberChatScript(member) {
  const now = Math.floor(Date.now() / 1000)
  const payload = {
    v: 1,
    iss: hostId,
    sub: String(member.id),
    jti: crypto.randomUUID(),
    name: member.name,
    email: member.email,
    iat: now,
    exp: now + 120,
  }
  const signingInput =
    base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })) +
    '.' +
    base64url(JSON.stringify(payload))
  const token =
    signingInput + '.' + crypto.createHmac('sha256', secret).update(signingInput).digest('base64url')
  return `<script src="${appOrigin}/embed.js" data-member-token="${token}"></script>`
}

app.get('/account', requireLogin, (req, res) => {
  res.setHeader('Cache-Control', 'private, no-store')
  // req.user 是網站自己的登入機制提供的會員資料
  res.render('account', { chatScript: memberChatScript(req.user) })
})

頁面樣板在 </body> 之前輸出 chatScript(依樣板語法,不要讓它被跳脫成文字)。

會員沒有登入的頁面

會員認證區登記的網址,也可以直接嵌入匿名聊天,不必另外登記。所以會員還沒登入時,輸出匿名嵌入那一行就好;登入後再改輸出帶 data-member-token 的版本。

小工具的語言

聊天小工具的介面與 AI 回覆,支援繁體中文、English、日本語三種,依下面的順序決定:

  1. <script> 的 data-locale 屬性
  2. 頁面 <html lang> 的值
  3. 後台該宿主站台的「聊天小工具預設語言」
  4. 全域設定的站台預設語言

前兩項的值,zh-TW、zh-Hant、zh-HK 視為繁體中文,en、en-US 這類視為英文,ja 視為日文;其他語言(包括簡體中文)會往下一項找。

網站本身是多語系的話,通常不必另外設定,小工具會跟著 <html lang> 走;單頁應用程式切換語言時只改了 <html lang>,小工具也會跟著切換。要固定某個語言,就在 <script> 加上 data-locale:

<script src="https://your-domain.example.com/embed.js" data-locale="ja"></script>

用了 data-locale 之後,小工具就不再跟著 <html lang> 變動。

下圖是 SupaTalker 官網切成日文後打開的小工具:介面跟著換成日文,熱門問題也顯示日文譯文,右上角標示「自動翻訳」(見下一節)。

網站切成日文,小工具跟著換成日文

其它:說明文章的自動翻譯

小工具「說明」分頁與首頁「熱門問題」裡的文章,來自全域設定勾選的知識庫分類。 這些文章只要寫一種語言,系統會自動翻成小工具支援的另外兩種語言。

日文介面的說明分頁:展開的答案下方有「自動翻訳 · 原文を表示」

  • 原文語言是站台預設語言,自動翻成另外兩種
  • 小工具以其他語言顯示時,文章標題與答案改用譯文;首頁熱門問題標示「自動翻譯」,說明分頁展開的答案下方標示「自動翻譯 · 顯示原文」,訪客可以按「顯示原文」切回原文(上圖日文介面為「自動翻訳 · 原文を表示」)
  • 每小時翻譯一次(整點 30 分),只翻新加入或改過的文章,所以新文章或剛修改的文章,在下一次翻譯完成前會先顯示原文
  • 翻譯失敗的文章維持顯示原文,並在安全規則與稽核記一筆事件

只有勾選分類裡、已發佈的知識庫文章會翻譯。對話中 AI 的回覆不受這裡影響,AI 本來就會用小工具目前的語言回答。

本頁內容