feat(gateway): webhook 渠道 InboundHandler 挂载到网关(渠道遗留2)
让 webhook 渠道的入站 HTTP 回调真正进运行时(此前 handler 建了但未挂载,回调不达 runtime)。 - channelbridge:Bridge.WebhookHandlers() map[string]http.Handler——遍历适配器,对暴露 InboundHandler()/Config() 的(*webhook.Adapter)收集 endpointPath→handler。 - gateway/server.go:Deps 加 Webhooks map[string]http.Handler;Server 存 webhooks; Handler() 挂载到 mux(无网关鉴权,各 handler 自带 HMAC 签名校验)。 - gateway/static.go:spa 加 isWebhookRoute——已注册的 webhook 路径交给 API mux(否则被 SPA fallback 吞成 index.html,webhook 回调不达)。 - wire.go:gateway.Deps 传 Webhooks: a.channelBridge.WebhookHandlers()(wireChannels 在 wireFrontend 前执行,bridge 已建好)。 实机验证:POST /webhook 空体→bad_request(400,webhook 解码响应非 SPA)、带 JSON→accepted(200)、 GET /webhook 被 webhook 路由处理。go build/vet/test 全绿。
This commit is contained in:
1 parent
adad4e1f62
commit
76134ed47e
4 files changed
+70
-1
No files matched your search
@@ -469,6 +469,7 @@ func (a *app) wireFrontend() error {
|
|||||||
RAG: ragStore,
|
RAG: ragStore,
|
||||||
Streaming: true,
|
Streaming: true,
|
||||||
Frontend: resolveFrontendFS(a.log),
|
Frontend: resolveFrontendFS(a.log),
|
||||||
|
Webhooks: a.channelBridge.WebhookHandlers(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("gateway: %w", err)
|
return fmt.Errorf("gateway: %w", err)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -28,6 +29,18 @@ import (
|
|||||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/user"
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
type webhookAdapter interface {
|
||||||
|
// InboundHandler returns the http.Handler the gateway mounts at the adapter's
|
||||||
|
// configured endpoint path 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
|
||||||
|
}
|
||||||
|
|
||||||
// Knob constants. Compile-time so no bare magic number appears in the run path
|
// Knob constants. Compile-time so no bare magic number appears in the run path
|
||||||
// (project rule §10).
|
// (project rule §10).
|
||||||
const (
|
const (
|
||||||
@@ -123,6 +136,33 @@ func (b *Bridge) Add(a adapter.PlatformAdapter) {
|
|||||||
b.adapters[a.Name()] = a
|
b.adapters[a.Name()] = a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebhookHandlers returns the inbound HTTP handlers of every registered adapter
|
||||||
|
// that exposes an HTTP callback (e.g. *webhook.Adapter), keyed by the endpoint
|
||||||
|
// path from the adapter's config. The gateway mounts these to receive external
|
||||||
|
// webhook callbacks into the runtime. A nil map is returned when no adapter
|
||||||
|
// provides an inbound handler.
|
||||||
|
func (b *Bridge) WebhookHandlers() map[string]http.Handler {
|
||||||
|
b.mu.RLock()
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
out := make(map[string]http.Handler)
|
||||||
|
for _, a := range b.adapters {
|
||||||
|
wh, ok := a.(webhookAdapter)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cfg := wh.Config()
|
||||||
|
path, _ := cfg["endpointPath"].(string)
|
||||||
|
if path == "" {
|
||||||
|
path = "/webhook"
|
||||||
|
}
|
||||||
|
out[path] = wh.InboundHandler()
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// Start connects every configured adapter, installs the inbound handler, and
|
// Start connects every configured adapter, installs the inbound handler, and
|
||||||
// launches the read loops. Adapters without credentials (or whose Connect fails)
|
// launches the read loops. Adapters without credentials (or whose Connect fails)
|
||||||
// are logged and left disconnected; they never fail startup. Start blocks only
|
// are logged and left disconnected; they never fail startup. Start blocks only
|
||||||
|
|||||||
@@ -199,6 +199,12 @@ type Deps struct {
|
|||||||
// When nil the gateway serves no static assets (API and WS only). The
|
// When nil the gateway serves no static assets (API and WS only). The
|
||||||
// embedded dist (internal/webui) or an external dist directory is used.
|
// embedded dist (internal/webui) or an external dist directory is used.
|
||||||
Frontend fs.FS
|
Frontend fs.FS
|
||||||
|
// Webhooks are externally-mounted inbound HTTP handlers (endpoint path →
|
||||||
|
// handler), typically from channel adapters that receive webhook callbacks
|
||||||
|
// (e.g. webhook). Each is mounted on the ServeMux without gateway auth; the
|
||||||
|
// handler itself enforces its own signature/authorization (webhook HMAC).
|
||||||
|
// Optional.
|
||||||
|
Webhooks map[string]http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server is the gateway. It owns a ServeMux aggregating REST + WS + admin and a
|
// Server is the gateway. It owns a ServeMux aggregating REST + WS + admin and a
|
||||||
@@ -222,6 +228,7 @@ type Server struct {
|
|||||||
voice VoiceService
|
voice VoiceService
|
||||||
streaming bool
|
streaming bool
|
||||||
frontend fs.FS
|
frontend fs.FS
|
||||||
|
webhooks map[string]http.Handler
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
hub *hub
|
hub *hub
|
||||||
}
|
}
|
||||||
@@ -254,6 +261,7 @@ func New(cfg *config.Config, deps Deps) (*Server, error) {
|
|||||||
rag: deps.RAG,
|
rag: deps.RAG,
|
||||||
streaming: deps.Streaming,
|
streaming: deps.Streaming,
|
||||||
frontend: deps.Frontend,
|
frontend: deps.Frontend,
|
||||||
|
webhooks: deps.Webhooks,
|
||||||
log: sourcedLog(deps.Log),
|
log: sourcedLog(deps.Log),
|
||||||
}
|
}
|
||||||
s.hub = newHub(s)
|
s.hub = newHub(s)
|
||||||
@@ -297,6 +305,15 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.Handle("POST /api/v1/admin/rag/delete", adminAuth(http.HandlerFunc(s.adminDeleteRag)))
|
mux.Handle("POST /api/v1/admin/rag/delete", adminAuth(http.HandlerFunc(s.adminDeleteRag)))
|
||||||
mux.Handle("POST /api/v1/admin/plugins/policy", s.auth.Authorize(auth.RoleOwner)(http.HandlerFunc(s.adminSetPolicy)))
|
mux.Handle("POST /api/v1/admin/plugins/policy", s.auth.Authorize(auth.RoleOwner)(http.HandlerFunc(s.adminSetPolicy)))
|
||||||
|
|
||||||
|
// External webhook callbacks (e.g. the webhook channel) are mounted without
|
||||||
|
// gateway auth; each handler enforces its own signature/authorization.
|
||||||
|
for path, hdl := range s.webhooks {
|
||||||
|
if path == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mux.Handle(path, hdl)
|
||||||
|
}
|
||||||
|
|
||||||
var h http.Handler = mux
|
var h http.Handler = mux
|
||||||
if s.frontend != nil {
|
if s.frontend != nil {
|
||||||
h = s.spa(h)
|
h = s.spa(h)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
func (s *Server) spa(api http.Handler) http.Handler {
|
func (s *Server) spa(api http.Handler) http.Handler {
|
||||||
fileServer := s.spaFileServer()
|
fileServer := s.spaFileServer()
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if isAPIRoute(r.URL.Path) {
|
if isAPIRoute(r.URL.Path) || s.isWebhookRoute(r.URL.Path) {
|
||||||
api.ServeHTTP(w, r)
|
api.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,17 @@ func (s *Server) spa(api http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isWebhookRoute reports whether path is a registered external webhook callback
|
||||||
|
// route. Such paths must reach the API mux (where the webhook handler is
|
||||||
|
// mounted) rather than the SPA file fallback, so inbound callbacks are served.
|
||||||
|
func (s *Server) isWebhookRoute(p string) bool {
|
||||||
|
if len(s.webhooks) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := s.webhooks[p]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// spaFileServer serves static assets, falling back to index.html for paths that
|
// spaFileServer serves static assets, falling back to index.html for paths that
|
||||||
// are not real files in the embedded/external dist.
|
// are not real files in the embedded/external dist.
|
||||||
func (s *Server) spaFileServer() http.Handler {
|
func (s *Server) spaFileServer() http.Handler {
|
||||||
|
|||||||
Reference in new issue
Block a user