mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 20:17:20 +00:00
feat: Mount channel webhook handlers directly on the main gateway.
This commit is contained in:
1 parent
18c783d1db
commit
6e7301ed94
5 files changed
+85
-7
No files matched your search
@@ -937,6 +937,14 @@ func runGateway() {
|
||||
// so the same routes are served on both the main listener and Tailscale.
|
||||
// Compiled via build tags: `go build -tags tsnet` to enable.
|
||||
mux := server.BuildMux()
|
||||
|
||||
// Mount channel webhook handlers on the main mux (e.g. Feishu /feishu/events).
|
||||
// This allows webhook-based channels to share the main server port.
|
||||
for _, route := range channelMgr.WebhookHandlers() {
|
||||
mux.Handle(route.Path, route.Handler)
|
||||
slog.Info("webhook route mounted on gateway", "path", route.Path)
|
||||
}
|
||||
|
||||
tsCleanup := initTailscale(ctx, cfg, mux)
|
||||
if tsCleanup != nil {
|
||||
defer tsCleanup()
|
||||
|
||||
@@ -11,6 +11,7 @@ package channels
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
||||
@@ -84,6 +85,17 @@ type StreamingChannel interface {
|
||||
OnStreamEnd(ctx context.Context, chatID string, finalText string) error
|
||||
}
|
||||
|
||||
// WebhookChannel extends Channel with an HTTP handler that can be mounted
|
||||
// on the main gateway mux instead of starting a separate HTTP server.
|
||||
// This allows webhook-based channels (e.g. Feishu/Lark) to share the main
|
||||
// server port, avoiding the need to expose additional ports in Docker.
|
||||
type WebhookChannel interface {
|
||||
Channel
|
||||
// WebhookHandler returns the HTTP handler and the path it should be mounted on.
|
||||
// Returns ("", nil) if the channel doesn't use webhook mode.
|
||||
WebhookHandler() (path string, handler http.Handler)
|
||||
}
|
||||
|
||||
// ReactionChannel extends Channel with status reaction support.
|
||||
// Channels that implement this interface can show emoji reactions on user messages
|
||||
// to indicate agent status (thinking, tool call, done, error, stall).
|
||||
|
||||
@@ -217,16 +217,41 @@ func (c *Channel) startWebSocket(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Channel) startWebhook(ctx context.Context) error {
|
||||
port := c.cfg.WebhookPort
|
||||
if port <= 0 {
|
||||
port = defaultWebhookPort
|
||||
// WebhookHandler returns the webhook HTTP handler and path for mounting on the main gateway mux.
|
||||
// Returns ("", nil) if not in webhook mode or if webhook_port > 0 (separate server).
|
||||
func (c *Channel) WebhookHandler() (string, http.Handler) {
|
||||
mode := c.cfg.ConnectionMode
|
||||
if mode != "webhook" {
|
||||
return "", nil
|
||||
}
|
||||
// Only mount on main mux when webhook_port is 0 (share main server port).
|
||||
if c.cfg.WebhookPort > 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
path := c.cfg.WebhookPath
|
||||
if path == "" {
|
||||
path = defaultWebhookPath
|
||||
}
|
||||
|
||||
handler := NewWebhookHandler(c.cfg.VerificationToken, c.cfg.EncryptKey, func(event *MessageEvent) {
|
||||
c.handleMessageEvent(context.Background(), event)
|
||||
})
|
||||
|
||||
return path, http.HandlerFunc(handler)
|
||||
}
|
||||
|
||||
func (c *Channel) startWebhook(ctx context.Context) error {
|
||||
// If webhook_port is 0, the handler is mounted on the main gateway mux
|
||||
// via WebhookHandler() — no separate server needed.
|
||||
if c.cfg.WebhookPort <= 0 {
|
||||
slog.Info("feishu: webhook handler mounted on main gateway mux", "path", c.webhookPath())
|
||||
return nil
|
||||
}
|
||||
|
||||
port := c.cfg.WebhookPort
|
||||
path := c.webhookPath()
|
||||
|
||||
slog.Info("feishu: starting Webhook server", "port", port, "path", path)
|
||||
|
||||
handler := NewWebhookHandler(c.cfg.VerificationToken, c.cfg.EncryptKey, func(event *MessageEvent) {
|
||||
@@ -312,6 +337,14 @@ func (c *Channel) sendMarkdownCard(ctx context.Context, chatID, receiveIDType, t
|
||||
return nil
|
||||
}
|
||||
|
||||
// webhookPath returns the configured webhook path or the default.
|
||||
func (c *Channel) webhookPath() string {
|
||||
if c.cfg.WebhookPath != "" {
|
||||
return c.cfg.WebhookPath
|
||||
}
|
||||
return defaultWebhookPath
|
||||
}
|
||||
|
||||
// --- Domain resolution ---
|
||||
|
||||
func resolveDomain(domain string) string {
|
||||
@@ -396,5 +429,6 @@ func (c *Channel) isDuplicate(messageID string) bool {
|
||||
return loaded
|
||||
}
|
||||
|
||||
// Ensure Channel implements the channels.Channel interface at compile time.
|
||||
// Ensure Channel implements the channels.Channel and WebhookChannel interfaces at compile time.
|
||||
var _ channels.Channel = (*Channel)(nil)
|
||||
var _ channels.WebhookChannel = (*Channel)(nil)
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -213,6 +214,29 @@ func (m *Manager) UnregisterChannel(name string) {
|
||||
delete(m.channels, name)
|
||||
}
|
||||
|
||||
// WebhookRoute holds a path and handler pair for mounting on the main gateway mux.
|
||||
type WebhookRoute struct {
|
||||
Path string
|
||||
Handler http.Handler
|
||||
}
|
||||
|
||||
// WebhookHandlers returns all webhook handlers from channels that implement WebhookChannel.
|
||||
// Used to mount webhook routes on the main gateway mux.
|
||||
func (m *Manager) WebhookHandlers() []WebhookRoute {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var routes []WebhookRoute
|
||||
for _, ch := range m.channels {
|
||||
if wh, ok := ch.(WebhookChannel); ok {
|
||||
if path, handler := wh.WebhookHandler(); path != "" && handler != nil {
|
||||
routes = append(routes, WebhookRoute{Path: path, Handler: handler})
|
||||
}
|
||||
}
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// SendToChannel delivers a message to a specific channel by name.
|
||||
func (m *Manager) SendToChannel(ctx context.Context, channelName, chatID, content string) error {
|
||||
m.mu.RLock()
|
||||
|
||||
@@ -77,8 +77,8 @@ export const configSchema: Record<string, FieldDef[]> = {
|
||||
feishu: [
|
||||
{ key: "domain", label: "Domain", type: "select", options: [{ value: "lark", label: "Lark (global) — webhook only" }, { value: "feishu", label: "Feishu (China)" }], defaultValue: "lark" },
|
||||
{ key: "connection_mode", label: "Connection Mode", type: "select", options: [{ value: "webhook", label: "Webhook" }, { value: "websocket", label: "WebSocket (Feishu only)" }], defaultValue: "webhook", help: "Lark Global only supports Webhook" },
|
||||
{ key: "webhook_port", label: "Webhook Port", type: "number", defaultValue: 3000, help: "Webhook mode only" },
|
||||
{ key: "webhook_path", label: "Webhook Path", type: "text", defaultValue: "/feishu/events", help: "Webhook mode only" },
|
||||
{ key: "webhook_port", label: "Webhook Port", type: "number", defaultValue: 0, help: "0 = share main gateway port (recommended)" },
|
||||
{ key: "webhook_path", label: "Webhook Path", type: "text", defaultValue: "/feishu/events", help: "Path on main server for Lark events" },
|
||||
{ key: "dm_policy", label: "DM Policy", type: "select", options: dmPolicyOptions, defaultValue: "pairing" },
|
||||
{ key: "group_policy", label: "Group Policy", type: "select", options: groupPolicyOptions, defaultValue: "open" },
|
||||
{ key: "require_mention", label: "Require @mention in groups", type: "boolean", defaultValue: true },
|
||||
|
||||
Reference in new issue
Block a user