@moosehq/game-client-sdk 參考
完整講解見生命週期上報指南。
這個包有一個根匯入和三個子路徑匯出:
@moosehq/game-client-sdk(根匯入) —— 你遊戲客戶端程式碼(執行在運營商 iframe 裡)使用的GameBridge類別。@moosehq/game-client-sdk/protocol—— 雙方約定的訊息契約。GameBridge內部會用到它;只有當你要在不透過GameBridge的情況下直接說這套協議時(例如寫一個扮演 shell 角色的測試工具),才需要直接匯入它。@moosehq/game-client-sdk/behavior——BehaviorCollector,機器人偵測遙測背後單純的互動時間彙整器。和GameBridge/protocol無關,完全不做任何postMessage,也不做任何網路 I/O。@moosehq/game-client-sdk/behavior-reporter——BehaviorReporter,負責接網路的對應角色:持有一個BehaviorCollector,並把它產生的摘要直接送給平台。實際接入時你會用的其實是它——見機器人偵測訊號。
所有金額欄位(amountMinor、balanceMinor、winAmountMinor)都是最小貨幣單位(例如分)的整數——和錢包 API 的慣例一致。
請注意,玩家要求的語言不屬於這套協議——GameBridge 只攜帶下面這些遊戲 → shell 方向的生命週期事件。語言是反方向、平台 → 遊戲的資訊——見啟動網址的 lang 查詢參數,或 VerifySessionResponse.config.language。
@moosehq/game-client-sdk
GameBridge
new GameBridge(options: GameBridgeOptions)| 選項 | 型別 | 說明 |
|---|---|---|
target | PostMessageTarget | 預設為 window.parent。主要在測試時才需要覆寫。 |
方法
notifyGameLoaded(balanceMinor: number): void—— 在你的遊戲載入完成後立即呼叫一次,帶上玩家目前的餘額(來自你自己的 session-verify/餘額查詢),讓 shell 能立刻顯示它,不必等到第一筆下注結算。notifyBetStart(amountMinor: number, balanceMinor: number): void—— 在這一局的結果已經確定(例如你伺服端的下注/結算呼叫剛返回)、即將開始揭曉它的時候呼叫一次——不是在下注之前。balanceMinor是這筆下注「扣款之後」、但「派彩套用之前」的餘額,讓 shell 能立刻看到扣款,而不會被提早揭露的派彩破壞你揭曉動畫的效果。如果下注被拒絕,完全跳過這個呼叫——因為根本沒有下注成功——直接呼叫notifyBetEnd。notifyBetEnd(outcome: 'win' | 'loss' | 'declined' | 'rolled_back', amountMinor: number, balanceMinor: number, winAmountMinor?: number): void—— 在你的揭曉動畫結束後呼叫;balanceMinor是最終、已完全結算的餘額。只有當outcome === 'win'時,winAmountMinor才有意義(也才應該傳入)。notifyBalanceExhausted(): voidnotifyExitGame(): void—— 玩家點選了遊戲內的「返回大廳」控制元件,遊戲無法關閉自己所在的 iframe,所以請求 shell 來處理。notifySessionRevoked(reason?: string): void—— 運營商在遊戲仍開啟時於伺服端結束了這個玩家的 session(POST /v1/operator/players/kick,詳見伺服端接入指南)。平台沒有直接推送到瀏覽器的通道——你是透過自己的方式得知這件事的(例如下一次錢包呼叫收到 401,或者你自建的伺服端推送通道;完整契約見Session 撤銷回呼,該契約通知的是你的伺服端)——並在這裡上報,讓 shell 可以顯示「您已被登出」並關閉 iframe,這和notifyExitGame代表的主動「返回大廳」不同。
@moosehq/game-client-sdk/protocol
shell 端沒有包裝類別——運營商直接針對這個模組實現自己的監聽器(參考監聽器範例見生命週期上報指南)。匯出內容:
// Game -> Shell —— 這套協議目前唯一的方向
type GameEvent =
| { type: 'GAME_LOADED'; balanceMinor: number }
| { type: 'BET_START'; amountMinor: number; balanceMinor: number }
| {
type: 'BET_END'
outcome: 'win' | 'loss' | 'declined' | 'rolled_back'
amountMinor: number
balanceMinor: number
winAmountMinor?: number
}
| { type: 'BALANCE_EXHAUSTED' }
| { type: 'EXIT_GAME' }
// 每條訊息都會被包在這個信封裡
const BRIDGE_SOURCE = 'moose-platform-game-bridge'
type Envelope<T> = { source: typeof BRIDGE_SOURCE; payload: T }
function wrapEnvelope<T>(payload: T): Envelope<T>
function isBridgeEnvelope(data: unknown): data is Envelope<unknown>
// GameBridge 的 target 選項接受的最小傳送介面——
// 實際使用會傳入 window.parent;測試時則注入樁物件。
type PostMessageTarget = { postMessage(message: unknown, targetOrigin: string): void }@moosehq/game-client-sdk/behavior
BehaviorCollector
new BehaviorCollector(options?: BehaviorCollectorOptions)
type BehaviorCollectorOptions = {
now?: () => number // 可注入的時鐘,用於測試;預設為 Date.now
}把瀏覽器裡的原始互動時間數據彙整成一份摘要,供平台的機器人偵測評分使用。它本身不做任何網路 I/O——搭配下面的 BehaviorReporter(或者直接用 BehaviorReporter,它自己就持有一個收集器)才能真正把摘要送給平台。完整走查見機器人偵測訊號。
方法
recordInteraction(event?: { isTrusted: boolean }): void—— 記錄一次互動。預設情況下,BehaviorReporter會替頁面上的每一次點擊自動呼叫這個方法(見下方它的autoCollectClicks選項);只有在你關閉了自動採集、想自己控管採集範圍或補充上報時,才需要直接呼叫它。盡量傳入觸發用的Event:真人點擊的isTrusted永遠是true,而程式化觸發的(例如腳本呼叫element.click())isTrusted是false,會被標記為自動化訊號。ready(): boolean—— 累積的訊號足夠回報時為true:要嘛已經觸發過某個自動化旗標,要嘛已經記錄了至少 3 次互動(2 次互動只能算出 1 個間隔,完全沒有變異數訊號)。takeDigest(): BehaviorDigest—— 把目前的統計視窗彙整成摘要並重置它,讓下一份摘要反映一個全新的視窗,而不是重複計算過去的互動。
type BehaviorDigest = {
intervalMeanMs: number // 記錄互動之間的平均毫秒間隔
intervalStdDevMs: number // 標準差——機器人的節奏通常又快又規律
automationFlags: string[] // 例如 "navigator_webdriver"、"untrusted_interaction_event"
}有兩個自動化旗標不需要時間視窗就能直接偵測:navigator_webdriver(navigator.webdriver === true,多數瀏覽器自動化工具會設定這個值)和 untrusted_interaction_event(任何一次記錄的互動,其 Event.isTrusted 為 false)。
@moosehq/game-client-sdk/behavior-reporter
BehaviorReporter
new BehaviorReporter(options: BehaviorReporterOptions)
type BehaviorReporterOptions = {
ingestUrl: string // 平台的行為採集端點——見「環境與基礎網址」
sessionToken: string // 以 X-Session-Token 標頭傳送;這個端點唯一的鑑權憑證
flushIntervalMs?: number // 預設 15000
collector?: BehaviorCollector // 可跨多個 reporter 共用同一個收集器,或注入測試用實例
collectorOptions?: BehaviorCollectorOptions // 未提供 collector 時,會轉送給 `new BehaviorCollector()`
fetchImpl?: typeof fetch // 可注入,用於測試。預設為全域 fetch
autoCollectClicks?: boolean // 預設 true——自動掛上一個 document 層級的 click 監聽器
}持有一個 BehaviorCollector,並且是真正跟網路對接的那一層:它把每份摘要直接 POST 到 ingestUrl,用 sessionToken 作為 X-Session-Token 標頭——和這個 SDK 生態圈裡其他呼叫不同,這不是一個簽名呼叫,因為瀏覽器沒辦法保存廠商的 HMAC 金鑰。全程都是盡力而為:任何失敗(網路錯誤、缺少 fetch、SSR/無 DOM 環境)都會被靜默吞掉,絕不會外顯給你的遊戲。
預設情況下(autoCollectClicks: true),建構函式會自動掛上一個 document 層級的 click 監聽器,把頁面上的每一次點擊都記錄為一次互動——除了建構這個 reporter 之外不需要任何其他接入動作。傳入 autoCollectClicks: false 可以跳過這個監聽器,改由你自己透過 recordInteraction 控管採集範圍。
方法
recordInteraction(event?: { isTrusted: boolean }): void—— 委派給底層的BehaviorCollector。autoCollectClicks開啟時(預設值)不需要呼叫這個方法——每一次點擊都已經透過建構函式掛上的監聽器送進收集器了。這個方法是給關閉了autoCollectClicks的呼叫方,或想記錄非 DOM 點擊互動的呼叫方使用的。flush(): void—— 若收集器已經累積足夠訊號(見BehaviorCollector.ready),立即送出目前的摘要,並重置統計視窗;訊號不足則什麼都不做。這個方法會在計時器、分頁隱藏(document.visibilitychange變成'hidden')、以及頁面卸載(window.pagehide)時自動執行——之所以也直接對外開放,是給想額外自訂觸發時機的呼叫方使用。stop(): void—— 取消計時器,並移除 document/window 上的監聽器(包含自動採集的 click 監聽器,如果有掛上的話)。遊戲被卸載時(例如 SPA 路由切換離開前)請呼叫這個方法,避免一個已經失效的 reporter 繼續觸發。
訊息安全性
需要做入站校驗的是 shell 端自己的監聽器(由運營商實現,不屬於這個套件)——它收到的每條訊息在 payload 被信任之前都應該經過三項檢查:
event.origin精確匹配預期的遊戲 origin。event.source確實是你所綁定的那個特定 iframe,而不只是同一個 origin 底下的任意視窗。isBridgeEnvelope(event.data)——payload 帶有BRIDGE_SOURCE信封標記。
只要有一項檢查沒通過,該訊息就應該被靜默忽略,而不是拋出例外——這不是一個「驗證失敗就報錯」的模型。同一頁面上其他不相關的 postMessage 流量(瀏覽器擴充功能、其他嵌入內容)是預期會出現的,你的監聽器對它們應該什麼都不做。