You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -23,12 +25,23 @@ import (
|
||||
"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
|
||||
)
|
||||
|
||||
// Encryptor 是 prefs handler 加密 im_webhook_url 所需的窄接口,由 crypto.Encryptor
|
||||
// 满足。以接口注入避免 notify 依赖 crypto 包。
|
||||
type Encryptor interface {
|
||||
Encrypt(plaintext []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// Handler 暴露 notify 模块的 HTTP 接口;依赖 Repository 与 SessionResolver。
|
||||
// 不直接持有 Dispatcher——发送通知由其它模块通过 Dispatcher 完成,handler
|
||||
// 仅服务 inbox 读取与状态变更。
|
||||
type Handler struct {
|
||||
repo Repository
|
||||
resolver middleware.SessionResolver
|
||||
// hub 为可选的实时推送中心;非 nil 时暴露 GET /stream SSE 端点。
|
||||
hub StreamHub
|
||||
// prefs/enc 为可选的偏好仓库与加密器;二者均非 nil 时暴露 GET/PUT /prefs。
|
||||
prefs PrefsRepository
|
||||
enc Encryptor
|
||||
}
|
||||
|
||||
// NewHandler 构造 notify 模块的 HTTP handler。
|
||||
@@ -36,6 +49,19 @@ func NewHandler(repo Repository, resolver middleware.SessionResolver) *Handler {
|
||||
return &Handler{repo: repo, resolver: resolver}
|
||||
}
|
||||
|
||||
// WithStreamHub 注入实时推送中心,启用 GET /api/v1/notifications/stream SSE。
|
||||
func (h *Handler) WithStreamHub(hub StreamHub) *Handler {
|
||||
h.hub = hub
|
||||
return h
|
||||
}
|
||||
|
||||
// WithPrefs 注入偏好仓库与加密器,启用 GET/PUT /api/v1/notifications/prefs。
|
||||
func (h *Handler) WithPrefs(prefs PrefsRepository, enc Encryptor) *Handler {
|
||||
h.prefs = prefs
|
||||
h.enc = enc
|
||||
return h
|
||||
}
|
||||
|
||||
// Mount 把 inbox 路由挂到给定 chi.Router 上。Auth 中间件在子路由内统一注入。
|
||||
func (h *Handler) Mount(r chi.Router) {
|
||||
r.Route("/api/v1/notifications", func(r chi.Router) {
|
||||
@@ -44,9 +70,74 @@ func (h *Handler) Mount(r chi.Router) {
|
||||
r.Get("/unread-count", h.unreadCount)
|
||||
r.Patch("/read-all", h.readAll)
|
||||
r.Patch("/{id}/read", h.markRead)
|
||||
if h.hub != nil {
|
||||
r.Get("/stream", h.stream)
|
||||
}
|
||||
if h.prefs != nil && h.enc != nil {
|
||||
r.Get("/prefs", h.getPrefs)
|
||||
r.Put("/prefs", h.putPrefs)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// stream 实现 GET /api/v1/notifications/stream:把该用户的实时通知以 SSE 推送。
|
||||
// 客户端用 EventSource 订阅;连接关闭时(r.Context().Done)自动退订。
|
||||
func (h *Handler) stream(w http.ResponseWriter, r *http.Request) {
|
||||
uid, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()),
|
||||
errs.New(errs.CodeUnauthorized, "未认证"))
|
||||
return
|
||||
}
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ch, cancel := h.hub.Subscribe(uid)
|
||||
defer cancel()
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
flusher.Flush()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case m, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
writeNotificationSSE(w, flusher, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeNotificationSSE 把一条通知序列化为 SSE 帧并 flush。
|
||||
func writeNotificationSSE(w http.ResponseWriter, f http.Flusher, m Message) {
|
||||
dto := notificationDTO{
|
||||
ID: m.ID.String(),
|
||||
Topic: m.Topic,
|
||||
Severity: string(m.Severity),
|
||||
Title: m.Title,
|
||||
Body: m.Body,
|
||||
Link: m.Link,
|
||||
Metadata: m.Metadata,
|
||||
CreatedAt: m.CreatedAt.UTC().Format(time.RFC3339),
|
||||
}
|
||||
if m.ReadAt != nil {
|
||||
s := m.ReadAt.UTC().Format(time.RFC3339)
|
||||
dto.ReadAt = &s
|
||||
}
|
||||
data, _ := json.Marshal(dto)
|
||||
fmt.Fprintf(w, "event: notification\ndata: %s\n\n", data)
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
// notificationDTO 是 inbox 单条通知的对外 JSON 形态。created_at/read_at
|
||||
// 统一序列化为 RFC3339 字符串;read_at 为 nil 时省略(omitempty)。
|
||||
type notificationDTO struct {
|
||||
|
||||
Reference in New Issue
Block a user