feat(agent-calendar): Agent 侧日历端点(可写,只能动自己建的)
在这之前 /calendar/* 全挂在 UserAuth 后面,Agent 密钥一律 401。 于是「明天九点提醒我看 CI」只能靠插件进程里的 setTimeout —— 进程一重启 定时器就消失,那条提醒静默不见且无处留痕。放进 Gateway 后由数据库与 调度器保证:插件重启、Agent 换机器、甚至换平台都不影响。 更重要的是它让**跨 Agent 的任务交接**成立:模型可以给 dsh 设一条 「明天交周报」的提醒。这件事模型自己做不到 —— 它没法让另一个进程在未来 某刻醒来。 三处收紧: **1. 只看/只改自己建的。** 别人的日程里可能有它无权知道的会议与地址。不存在与不属于我都回 **404** 而非 403 —— 后者会泄漏「这个 id 存在」,让 Agent 能枚举出别人有多少条日程。 **2. 不能设给人类。** 理由是投递通道不对等。Agent 之间的提醒是任务信号:收到就干活、干完回信。 发给人的提醒是打扰 —— 进收件箱、触发未读徽标,而人**无法回信让它停下** (提醒是日历实体不是对话),只能去 WebUI 里找出那条事件删掉。 一个 Agent 建条「每 10 分钟提醒 jianf 检查进度」的代价远大于收益 (它其实可以直接 send_mail)。 **3. 速率 + 总量双闸。** 速率(20 次/小时,独立桶)压住「短时间暴建」,压不住「每小时建 19 条、 连建一周」—— 而日历事件是**长效**的,一条每日重复提醒会一直发下去。 攒下 300 条之后即使停止建新的,每天仍有 300 封提醒涌出来。 所以加 maxActiveEventsPerAgent=50,并在列表响应里回传 active_limit: 模型看到 42/50 就知道该清理,只在撞墙时才报错等于让它一直蒙在鼓里。 其他设计点: - **PUT 是部分更新**(人类端点是整体替换)。调用方是模型 —— 要求它每次 回传全部字段,漏一个就把提醒正文或收件人清空,而那种破坏没有任何报错。 全部字段用 *T,nil = 没传 = 保持原值。 - 一次性事件设在过去拦掉(会立刻触发,几乎总是时区或年份写错); 重复事件不拦 —— 「每天 9 点」从昨天开始是合理写法。 - 收件人省略时默认给自己:最常见的用法,每次要求写出自己的名字只会让 模型忘记然后拿到 400。 顺带:两个 handler 文件各写了一份手写 itoa(models_scope 那份还漏了负数), 统一成 strconv.Itoa。 测试 repo 9 例:创建者过滤、收件人≠创建者不返回、总量计数、 速率桶与会话桶独立、按 Agent 隔离、失败归还名额。 生产实测 11 项全过(含 403/400/404/429 各条边界)。
This commit is contained in:
@ -16,6 +16,7 @@ import (
|
||||
"github.com/agentmail/gateway/internal/handler"
|
||||
"github.com/agentmail/gateway/internal/middleware"
|
||||
"github.com/agentmail/gateway/internal/repo"
|
||||
"github.com/agentmail/gateway/internal/scheduler"
|
||||
"github.com/agentmail/gateway/internal/static"
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimw "github.com/go-chi/chi/v5/middleware"
|
||||
@ -60,6 +61,11 @@ func main() {
|
||||
// 否则取消发信与 Agent 崩溃留下的文件会让磁盘单调增长。
|
||||
go sweepOrphanAttachments(blobs)
|
||||
|
||||
// 日历调度器:把到期的提醒变成邮件。
|
||||
// 必须在建表(migrate)之后启动 —— 它启动时立即扫一次表。
|
||||
scheduler.Start()
|
||||
defer scheduler.Stop()
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(chimw.Logger)
|
||||
@ -127,6 +133,20 @@ func main() {
|
||||
r.Get("/agent/mail/{id}", handler.AgentGetMail)
|
||||
r.Get("/agent/mail/{id}/thread", handler.AgentGetMailThread)
|
||||
r.Get("/agent/sessions/{id}/participants", handler.AgentSessionParticipants)
|
||||
|
||||
// ---- 日历 / 待办(可写,但只能动自己建的)----
|
||||
//
|
||||
// 在这之前「明天九点提醒我看 CI」只能靠插件进程里的 setTimeout ——
|
||||
// 进程一重启定时器就消失,提醒静默不见且无处留痕。放进 Gateway 之后
|
||||
// 由数据库与调度器保证:插件重启、Agent 换机器都不影响。
|
||||
//
|
||||
// 三处收紧(见 handler/agent_calendar.go):只看/只改自己建的、
|
||||
// 不能设给人类、速率 20 次每小时 + 总量 50 条双闸。
|
||||
r.Post("/agent/calendar/events", handler.AgentCreateCalendarEvent)
|
||||
r.Get("/agent/calendar/events", handler.AgentListCalendarEvents)
|
||||
r.Get("/agent/calendar/events/{id}", handler.AgentGetCalendarEvent)
|
||||
r.Put("/agent/calendar/events/{id}", handler.AgentUpdateCalendarEvent)
|
||||
r.Delete("/agent/calendar/events/{id}", handler.AgentDeleteCalendarEvent)
|
||||
})
|
||||
|
||||
// ---- 人类登录态 ----
|
||||
@ -178,6 +198,19 @@ func main() {
|
||||
// 在线 Agent 列表(补全用)
|
||||
r.Get("/agents", handler.ListAgents)
|
||||
|
||||
// 日历事件
|
||||
r.Post("/calendar/events", handler.CreateCalendarEvent)
|
||||
r.Get("/calendar/events", handler.ListCalendarEvents)
|
||||
r.Get("/calendar/events/{id}", handler.GetCalendarEvent)
|
||||
r.Put("/calendar/events/{id}", handler.UpdateCalendarEvent)
|
||||
r.Delete("/calendar/events/{id}", handler.DeleteCalendarEvent)
|
||||
r.Post("/calendar/events/{id}/attachments", handler.UploadCalendarAttachment)
|
||||
r.Get("/calendar/events/{id}/attachments", handler.ListCalendarAttachments)
|
||||
// 单条删除:撤一个错传的文件不该要求把整条日程重建
|
||||
r.Delete("/calendar/attachments/{attachmentID}", handler.DeleteCalendarAttachment)
|
||||
r.Get("/calendar/export.ics", handler.ExportCalendarICS)
|
||||
r.Post("/calendar/import.ics", handler.ImportCalendarICS)
|
||||
|
||||
// 管理员
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.AdminOnly)
|
||||
|
||||
Reference in New Issue
Block a user