feat(channel): KOOK Webhook(WH) 接收模式——与 WS 配置切换(ReceiveMode)
- kook.Config 加 ReceiveMode("ws"默认/"webhook") + WebhookPath(默认/webhook/kook) + WebhookVerifyToken + WebhookEncryptKey(AES-256-CBC)
- 按权威文档(Reference/kaiheila webhook.md)实现: 无X-Signature HMAC, 真实校验=d.verify_token比对(不匹配403)+可选AES解密+zlib解压+challenge回显
- webhook.go: InboundHandler+WebhookPath, POST→zlib解压→AES解密→verify_token校验→challenge回显→归一UnifiedMessage→SetMessageHandler; WS模式Connect不拨网关仅标记connected
- channelbridge/webhookAdapter 接口改用 WebhookPath(); WebhookHandlers 收KOOK WH(ws模式跳过); 既有webhook通道补WebhookPath照常挂载
- namedAdapter 转发InboundHandler/WebhookPath; kook env兜底 +4个CHANNEL_KOOK_*键
- 测试: verify_token 403/challenge/zlib/AES往返/WebhookPath按模式/WH不拨gateway/WebhookHandlers收KOOK; 全仓build/vet/test绿
- 待真token端到端实测(需主人提供CHANNEL_KOOK_TOKEN+开发者后台切WebHook配回调URL)
This commit is contained in:
1 parent
1ebc82fffd
commit
617157262b
11 files changed
+981
-32
No files matched your search
@@ -31,15 +31,16 @@ import (
|
||||
)
|
||||
|
||||
// webhookAdapter is the optional capability a channel adapter may expose to
|
||||
// source an HTTP inbound callback. *webhook.Adapter implements it via
|
||||
// InboundHandler(). The bridge reports these so the gateway can mount them.
|
||||
// source an HTTP inbound callback. *webhook.Adapter and *kook.Adapter (webhook
|
||||
// mode) implement it. The bridge reports these so the gateway can mount them.
|
||||
type webhookAdapter interface {
|
||||
// InboundHandler returns the http.Handler the gateway mounts at the adapter's
|
||||
// configured endpoint path to receive external callbacks.
|
||||
// InboundHandler returns the http.Handler the gateway mounts at the route to
|
||||
// receive external callbacks.
|
||||
InboundHandler() http.Handler
|
||||
// Config returns the adapter config snapshot; the endpoint path is read from
|
||||
// it so the gateway can register the correct route.
|
||||
Config() map[string]any
|
||||
// WebhookPath returns the route the gateway should mount the handler at. It
|
||||
// returns "" when the adapter is not currently exposing a webhook, letting the
|
||||
// bridge skip it (e.g. a websocket-mode KOOK).
|
||||
WebhookPath() string
|
||||
}
|
||||
|
||||
// Knob constants. Compile-time so no bare magic number appears in the run path
|
||||
@@ -229,12 +230,17 @@ func (b *Bridge) WebhookHandlers() map[string]http.Handler {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cfg := wh.Config()
|
||||
path, _ := cfg["endpointPath"].(string)
|
||||
path := wh.WebhookPath()
|
||||
if path == "" {
|
||||
path = "/webhook"
|
||||
// Adapter is not currently exposing a webhook (e.g. a websocket-mode
|
||||
// KOOK); skipping avoids mounting a handler at a stale or colliding route.
|
||||
continue
|
||||
}
|
||||
out[path] = wh.InboundHandler()
|
||||
h := wh.InboundHandler()
|
||||
if h == nil {
|
||||
continue
|
||||
}
|
||||
out[path] = h
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/user"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/kook"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/webhook"
|
||||
)
|
||||
|
||||
// fakeAdapter is a minimal PlatformAdapter that records outbound sends so the
|
||||
@@ -398,7 +400,7 @@ func TestBlacklistGroupMessageDroppedBeforeLoop(t *testing.T) {
|
||||
// A stranger's group session that would otherwise be processed.
|
||||
sess := &stubSessions{sess: session.Session{SessionID: "sess-g", IsGroup: true, Owner: false, MemWritable: false}}
|
||||
ad := &fakeAdapter{name: "telegram", cfg: map[string]any{
|
||||
"bot_token": "configured",
|
||||
"bot_token": "configured",
|
||||
"denyGroups": []string{"-100"},
|
||||
}}
|
||||
var rejected []string
|
||||
@@ -438,7 +440,7 @@ func TestBlacklistUserMessageDroppedBeforeLoop(t *testing.T) {
|
||||
chat := &stubChatter{res: &loop.Result{Text: "不该回复"}}
|
||||
sess := &stubSessions{sess: session.Session{SessionID: "sess-s", IsGroup: false, Owner: false, MemWritable: false}}
|
||||
ad := &fakeAdapter{name: "telegram", cfg: map[string]any{
|
||||
"bot_token": "configured",
|
||||
"bot_token": "configured",
|
||||
"allowUsers": []string{"111"},
|
||||
}}
|
||||
b, err := New(Options{Loop: chat, Sessions: sess, Owner: &user.OwnerProfile{}, History: &captureHistory{}})
|
||||
@@ -555,3 +557,53 @@ func TestNoHistoryStoreKeepsBridgeUnchanged(t *testing.T) {
|
||||
t.Fatalf("loop session id = %q, want %q", chat.sessionID, history.MainSessionID)
|
||||
}
|
||||
}
|
||||
|
||||
// newWebhookBridge builds a bare bridge (no adapters) wired with the stubs the
|
||||
// other tests use, so a WebhookHandlers test can add adapters explicitly.
|
||||
func newWebhookBridge(t *testing.T) *Bridge {
|
||||
t.Helper()
|
||||
chat := &stubChatter{res: &loop.Result{Text: "hi"}}
|
||||
sess := &stubSessions{sess: session.Session{SessionID: "sess-1", Owner: true, MemWritable: true}}
|
||||
b, err := New(Options{Loop: chat, Sessions: sess, Owner: &user.OwnerProfile{}})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestWebhookHandlersCollectsKookWebhook(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := newWebhookBridge(t)
|
||||
b.Add(kook.New(kook.Config{BotToken: "t", ReceiveMode: "webhook", WebhookPath: "/webhook/kook"}))
|
||||
|
||||
handlers := b.WebhookHandlers()
|
||||
h, ok := handlers["/webhook/kook"]
|
||||
if !ok || h == nil {
|
||||
t.Fatalf("kook webhook handler not mounted; got %v", handlers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookHandlersSkipsWSModeKook(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := newWebhookBridge(t)
|
||||
b.Add(kook.New(kook.Config{BotToken: "t", ReceiveMode: "ws"}))
|
||||
|
||||
if got := b.WebhookHandlers(); got != nil {
|
||||
t.Fatalf("ws-mode kook exposed a webhook handler, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookHandlersMountsWebhookChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := newWebhookBridge(t)
|
||||
b.Add(webhook.New(webhook.WithConfig(webhook.Config{EndpointPath: "/cb"})))
|
||||
|
||||
handlers := b.WebhookHandlers()
|
||||
h, ok := handlers["/cb"]
|
||||
if !ok || h == nil {
|
||||
t.Fatalf("webhook channel handler not mounted; got %v", handlers)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user