// Package kook implements the built-in KOOK (KaiHeiLa) channel adapter for // Cyrene v2. KOOK is a stateful channel with two mutually exclusive inbound // modes: a WebSocket-gateway mode (events over a gateway WebSocket with // hello/heartbeat/event/resume signaling) and a Webhook mode (KOOK POSTs event // callbacks to a Cyrene gateway route). Outbound messages are always sent over a // bot-token-authenticated REST API (`Authorization: Bot `). // // The adapter conforms to internal/channel/adapter.PlatformAdapter, translating // native KOOK events into the normalized adapter.UnifiedMessage model and // delivering replies via /api/v3/message/create (channels) and // /api/v3/direct-message/create (private chat). Identity is carried as channel // actor metadata (author id + nickname) only — owner/member resolution is the // channel layer's job, never a prompt-text heuristic. // // The design authority is DevDocs/design/08-渠道接入.md §4 (kook) and §7 // (framework reusability). It depends on internal/channel/adapter (including the // shared wsutil and httpclient primitives), internal/logger, and the standard // library. The bot token, verify token and encrypt key are never logged. package kook import ( "strings" "time" ) // Defaults and knobs for the KOOK integration. const ( // defaultBaseURL is the KOOK REST API root. defaultBaseURL = "https://www.kookapp.cn/api/v3" // defaultHTTPTimeout bounds a single REST request. defaultHTTPTimeout = 30 * time.Second // gatewayPath is the gateway-address fetch endpoint relative to baseURL. gatewayPath = "/gateway/index" // maxMessageLen is KOOK's documented per-message content limit. maxMessageLen = 8000 // heartbeatInterval is how often the client sends a ping (KOOK: 30s). heartbeatInterval = 30 * time.Second // helloTimeout bounds the wait for the handshake (hello/resume ack). helloTimeout = 6 * time.Second ) // Inbound receive modes. KOOK's webhook and websocket modes are mutually // exclusive (DevDocs/design/08 §4): selecting webhook means the gateway WebSocket // no longer delivers events, and vice versa. ReceiveMode decides which path the // adapter uses; the two never run together. const ( // modeWS is the default: connect the KOOK gateway WebSocket and receive events. modeWS = "ws" // modeWebhook exposes a Cyrene gateway route that KOOK POSTs callbacks to. modeWebhook = "webhook" // defaultWebhookPath is used when Config.WebhookPath is empty in webhook mode. defaultWebhookPath = "/webhook/kook" // defaultMaxBodyBytes caps an inbound webhook payload at 1 MiB. defaultMaxBodyBytes = 1 << 20 ) // Config holds the settings required to reach the KOOK API. type Config struct { // BotToken is the secret bot token issued by the KOOK developer center. It is // surfaced through Config() but never written to logs. BotToken string `json:"bot_token"` // BaseURL is the KOOK REST API root; empty uses the production default. BaseURL string `json:"base_url"` // ReceiveMode selects the inbound delivery path: "ws" (default) connects the // KOOK gateway WebSocket; "webhook" exposes a gateway route that KOOK POSTs // events to. The two are mutually exclusive. ReceiveMode string `json:"receive_mode"` // WebhookPath is the route the Cyrene gateway mounts the KOOK webhook handler // at when ReceiveMode == "webhook". Empty uses defaultWebhookPath. WebhookPath string `json:"webhook_path"` // WebhookVerifyToken is the KOOK developer-center verify token. When set, every // webhook callback's d.verify_token must match or it is rejected with 403. WebhookVerifyToken string `json:"webhook_verify_token"` // WebhookEncryptKey is the KOOK Encrypt Key. When set, an encrypted callback // body ({"encrypt": ...}) is decrypted with AES-256-CBC before parsing. WebhookEncryptKey string `json:"webhook_encrypt_key"` } // normalizedReceiveMode returns ReceiveMode, defaulting empty to "ws". func (c Config) normalizedReceiveMode() string { if strings.TrimSpace(c.ReceiveMode) == "" { return modeWS } return strings.TrimSpace(c.ReceiveMode) } // IsWebhook reports whether the adapter should run in webhook mode. func (c Config) IsWebhook() bool { return c.normalizedReceiveMode() == modeWebhook } // Validate checks the configuration for values needed to connect. The bot token // is required in both modes because outbound (reply) delivery always uses the // bearer REST API. func (c Config) Validate() error { if strings.TrimSpace(c.BotToken) == "" { return ErrMissingToken } return nil }