Files
MailUI4Agents/server/internal/handler/events.go
JianFeeeee a8d6811cd7 style(server): gofmt 注释规范 + init.sql 补 parent_session_id + 新增 ratelimit 判据
## 主体是 gofmt

`server/` 下 20 个文件的改动里,**142/147 行是注释与缩进** ——
Go 1.19+ 的 gofmt 把块注释里的 `//   x` 规范成 `//\tx`。
功能零变化。

## 一处实质:init.sql 补 parent_session_id

`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS parent_session_id
 TEXT REFERENCES sessions(session_id)`

远端已在 `db/migrate.go` 的 `sqliteAddColumns` 结构迁移通道里加了同一列;
init.sql 是**全新库**的建库脚本,缺这列会让新库少一个字段。

- `IF NOT EXISTS` ⇒ 与结构迁移通道幂等,两者并存无害
- 默认空 = 根节点;**不据历史数据猜父子**(猜错会让线索拓扑失真,比没有更糟)

## 新增 handler/ratelimit_test.go(162 行)

速率限制的判据。此前这一块没有独立判据。

## 验证(本机实跑,非推断)

    go build ./...                              通过
    go vet ./internal/{handler,repo,db,sse}/   通过
    go test -count=1(handler/repo/db/sse/blob)
      handler  41.3s  ok
      repo    101.9s  ok
      db       1.1s  ok
      sse      0.01s ok
      blob     0.08s ok

`-count=1` 是刻意的:第一遍跑出来全是 `(cached)`,那种绿灯不算数。
2026-10-05 09:26:59 +08:00

93 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
import (
"net/http"
"github.com/agentmail/gateway/internal/middleware"
"github.com/agentmail/gateway/internal/repo"
"github.com/agentmail/gateway/internal/sse"
)
// GET /api/v1/events/stream
//
// 四种凭证,都必须真正验证过身份才能订阅:
//
// Authorization: Bearer <agent_key_token> → Agent 通道(密钥认证)
// X-Agent-Name + X-Agent-Secret → Agent 通道(旧方式,兼容)
// 登录 Cookie 或 Bearer <user_key_token> → 人类用户通道
// ?access_token=<token> → 浏览器 EventSource 专用回退
//
// 注意不能只凭 X-Agent-Name 就分流:那等于任何人报个名字就能读走别人的新邮件通知。
// query 令牌仅本端点接受(EventSource 无法带自定义头),其余接口一律要求请求头,
// 因为 URL 里的令牌会进访问日志与 Referer。
func SSEStream(w http.ResponseWriter, r *http.Request) {
agentName, ok := resolveStreamAgent(r)
if !ok {
Error(w, http.StatusUnauthorized, "凭证无效")
return
}
userName := ""
if agentName == "" {
u := middleware.OptionalUserWithQuery(r)
if u == nil {
Error(w, http.StatusUnauthorized, "not authenticated")
return
}
userName = u.Username
}
client := sse.Default.AddClient(w, r, agentName, userName)
if client == nil {
Error(w, http.StatusInternalServerError, "SSE not supported")
return
}
<-r.Context().Done()
sse.Default.RemoveClient(client.ID)
}
// resolveStreamAgent 校验 Agent 侧凭证。
// 返回 ("", true) 表示这不是 Agent 请求,交给人类用户分支;
// 返回 ("", false) 表示带了 Agent 凭证但验证失败。
func resolveStreamAgent(r *http.Request) (string, bool) {
// 密钥认证:Bearer 令牌可能是 Agent 密钥,也可能是用户密钥。
// 先按 Agent 密钥试,失败就落到人类分支(那里会再按用户密钥试)。
token := middleware.BearerToken(r)
if token == "" {
token = middleware.QueryToken(r) // EventSource 回退
}
if token != "" {
name, err := repo.VerifyAgentKey(r.Context(), token)
if err == nil && name != "" {
return name, true
}
return "", true
}
name := r.Header.Get("X-Agent-Name")
if name == "" {
name = r.URL.Query().Get("agent_name")
}
if name == "" {
return "", true // 非 Agent 请求
}
secret := r.Header.Get("X-Agent-Secret")
if secret == "" {
return "", false // 报了名字却没给凭证
}
agent, err := repo.VerifyAgent(r.Context(), name, secret)
if err != nil {
return "", false
}
return agent.Name, true
}
// GET /api/v1/events/status
func SSEStatus(w http.ResponseWriter, r *http.Request) {
JSON(w, http.StatusOK, map[string]interface{}{
"connected_clients": sse.Default.ClientCount(),
})
}