程式碼整合
給網站開發人員:匿名嵌入的一行 script、會員認證嵌入的入場票與會員資料、小工具的語言,以及說明文章的自動翻譯
這一頁寫給負責網站的開發人員:從後台複製下來的嵌入碼與程式片段,要放在網站的哪裡、怎麼改。 網站的網址要先在宿主站台登記好,沒有登記的網站,放了嵌入碼也無法使用。
兩種嵌入,差在要不要後端
- 匿名嵌入 —— 只要在網頁加一行
<script>,靜態網頁也可以 - 會員認證嵌入 —— 網站後端要在會員登入後簽一張「入場票」,放進那一行
<script>。適合已經有會員登入與會員資料的網站
以下範例的 https://your-domain.example.com 代表 SupaTalker 後台的網址,Host ID 與 secret 也是示意值,一律以後台複製下來的為準。
匿名嵌入
在後台「匿名嵌入」區登記網址後,按「複製嵌入碼」,把這一行貼到每個要出現聊天小工具的頁面,放在 </body> 之前:
<!doctype html>
<html lang="zh-Hant">
<body>
<!-- 網站原本的內容 -->
<script src="https://your-domain.example.com/embed.js"></script>
</body>
</html>頁面右下角就會出現氣泡按鈕,訪客點它打開聊天小工具。以下是 SupaTalker 官網實際嵌入的樣子:


氣泡的顏色、形狀、位置與名稱由後台的全域設定統一決定,不必在這一行加任何屬性;後台改了之後約 1 分鐘生效,不用重貼。
會員認證嵌入
適合已經有會員登入機制、用 Node、PHP、Python 等後端語言建立的網站。運作方式:
- 會員登入網站後,網站後端用 Host ID 與 secret 簽一張入場票
- 把入場票放進
<script>的data-member-token屬性,輸出在頁面上 - 聊天小工具用這張票確認會員身分,訪客就以會員身分對話,對話紀錄也跟著會員保留
在後台「會員認證嵌入」區建立站台後,該站台的「複製 Node 片段」「複製 PHP 片段」「複製 Python 片段」會複製已經填好 Host ID 與 secret 的程式片段,內容如下:
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
$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>';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)為例,把片段整理成一個函式,在會員登入後的頁面輸出:
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、日本語三種,依下面的順序決定:
<script>的data-locale屬性- 頁面
<html lang>的值 - 後台該宿主站台的「聊天小工具預設語言」
- 全域設定的站台預設語言
前兩項的值,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 本來就會用小工具目前的語言回答。