Files
MailUI4Agents/server/internal/handler/contacts.go

222 lines
6.0 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/models"
"github.com/agentmail/gateway/internal/repo"
"github.com/agentmail/gateway/internal/sse"
"github.com/google/uuid"
)
// ---------- Contacts左侧联系人界面按登录用户隔离 ----------
// GET /api/v1/contacts?archived=false
func ListContacts(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
Error(w, http.StatusUnauthorized, "not authenticated")
return
}
archived := r.URL.Query().Get("archived") == "true"
// 管理员可用 ?all=true 查看全部
scope := user.Username
if user.IsAdmin() && r.URL.Query().Get("all") == "true" {
scope = ""
}
contacts, err := repo.ListContactsFor(r.Context(), scope, archived)
if err != nil {
Error(w, http.StatusInternalServerError, "Failed to list contacts")
return
}
JSON(w, http.StatusOK, map[string]interface{}{
"contacts": emptySlice(contacts),
})
}
type archiveRequest struct {
Address string `json:"address"`
SessionID string `json:"session_id"`
}
// POST /api/v1/contacts/archive
// 归档指定 name@path.sessionAgent 侧会话归档 + 邮箱界面移除
func ArchiveContact(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
Error(w, http.StatusUnauthorized, "not authenticated")
return
}
var req archiveRequest
if !DecodeBody(w, r, &req) {
return
}
var sessionID uuid.UUID
switch {
case req.SessionID != "":
id, err := uuid.Parse(req.SessionID)
if err != nil {
Error(w, http.StatusBadRequest, "Invalid session_id")
return
}
sessionID = id
case req.Address != "":
addr, err := models.ParseAddress(req.Address)
if err != nil {
Error(w, http.StatusBadRequest, "Invalid address: "+err.Error())
return
}
id, err := repo.FindSessionByAddress(r.Context(), addr.Name, addr.Path, addr.Session)
if err != nil {
Error(w, http.StatusNotFound, "No session matches "+req.Address)
return
}
sessionID = id
default:
Error(w, http.StatusBadRequest, "Provide address or session_id")
return
}
// 鉴权:只能归档自己参与的会话(管理员不限)
allowed, err := repo.UserCanAccessSession(r.Context(), user, sessionID)
if err != nil {
Error(w, http.StatusInternalServerError, "Failed to check permission")
return
}
if !allowed {
Error(w, http.StatusForbidden, "无权归档他人的会话")
return
}
session, err := repo.GetSessionByID(r.Context(), sessionID)
if err != nil {
Error(w, http.StatusNotFound, "Session not found")
return
}
mails, _ := repo.GetSessionMails(r.Context(), sessionID)
if err := repo.ArchiveSession(r.Context(), sessionID); err != nil {
Error(w, http.StatusInternalServerError, "Failed to archive session")
return
}
alias := ""
if session.Alias != nil {
alias = *session.Alias
}
payload := map[string]interface{}{
"session_id": sessionID.String(),
"session_alias": alias,
"archived_by": user.Username,
}
// 通知会话内所有参与方Agent 与人类),各自归档/移除
notified := map[string]bool{}
for _, m := range mails {
names := append([]string{m.FromName, m.ToName}, ccNames(m.CCList)...)
for _, name := range names {
if name == "" || notified[name] {
continue
}
notified[name] = true
sse.Default.SendToRecipient(name, "session_archived", payload)
}
}
if !notified[user.Username] {
sse.Default.SendToUser(user.Username, "session_archived", payload)
}
JSON(w, http.StatusOK, map[string]interface{}{
"status": "archived",
"session_id": sessionID.String(),
"session_alias": alias,
})
}
// GET /api/v1/contacts/suggest?name=xxx&path=yyy
// 三段式补全:无 name 给 Agent+人类用户名;有 name 给工作区;两者都有给会话别名(含 new)
func SuggestAddress(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r)
if user == nil {
Error(w, http.StatusUnauthorized, "not authenticated")
return
}
name := r.URL.Query().Get("name")
path := r.URL.Query().Get("path")
if name == "" {
agents, err := repo.ListAgents(r.Context(), "")
if err != nil {
Error(w, http.StatusInternalServerError, "Failed to list agents")
return
}
users, _ := repo.ListActiveUsernames(r.Context())
names := make([]string, 0, len(agents)+len(users))
for _, a := range agents {
names = append(names, a.Name)
}
for _, u := range users {
if u == user.Username {
continue // 不建议给自己发信
}
names = append(names, u)
}
JSON(w, http.StatusOK, map[string]interface{}{
"kind": "name",
"suggestions": emptySlice(names),
})
return
}
if path == "" {
// 人类用户没有工作区,直接给空列表(前端会继续走 session 段)
paths, _ := repo.SuggestPaths(r.Context(), name)
JSON(w, http.StatusOK, map[string]interface{}{
"kind": "path",
"suggestions": emptySlice(paths),
})
return
}
sessions, err := repo.SuggestSessionCandidates(r.Context(), user.Username, name, path)
if err != nil {
Error(w, http.StatusInternalServerError, "Failed to suggest sessions")
return
}
// suggestions 保留纯字符串形式:已部署的前端与第三方客户端只认这一个字段。
// 带标题与来源的完整形式另放 candidates两个字段同序。
aliases := make([]string, 0, len(sessions)+1)
for _, c := range sessions {
aliases = append(aliases, c.Alias)
}
// new 总是可选且永远在最后:它不是一条已存在的会话,
// 排在前面会让人在想续谈时随手回车开出一条新线索。
aliases = append(aliases, "new")
sessions = append(sessions, repo.SessionCandidate{Alias: "new", Source: "new", Title: "新建会话"})
JSON(w, http.StatusOK, map[string]interface{}{
"kind": "session",
"suggestions": aliases,
"candidates": sessions,
})
}
func ccNames(list []models.Address) []string {
out := make([]string, 0, len(list))
for _, a := range list {
if a.Name != "" {
out = append(out, a.Name)
}
}
return out
}