Files
MailUI4Agents/server/internal/repo/participants.go

136 lines
4.3 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 repo
import (
"context"
"encoding/json"
"sort"
"github.com/agentmail/gateway/internal/db"
"github.com/agentmail/gateway/internal/models"
"github.com/google/uuid"
)
// Participant 是一条会话里的一个参与方。
//
// Path 是该参与方**自己那个地址的 path 位**,不是别人的:一封主发给 dsh@/b、
// 抄送给 opencode@/a 的邮件里,两人的工作目录不同,混用会让对方在别人的目录里
// 开会话(生产上已发生过一次,见 PLUGIN-CONTRACT 9.3)。
type Participant struct {
Name string `json:"name"`
Path string `json:"path"`
// Roles 是该参与方在这条会话里出现过的全部身份from / to / cc 的并集。
// 用集合而非单值:同一个人常常既发过信也被抄送过,只留最后一个身份会让
// 「谁是这件事的负责人」这个判断出错。
Roles []string `json:"roles"`
// MailCount 是该参与方作为发件人的邮件数。用来回答「谁还没回」——
// 参与方列表里 from 计数为 0 的那个就是还没开口的人。
MailCount int `json:"mail_count"`
}
// SessionParticipants 列出会话的全部参与方及各自的地址素材。
//
// 为什么要逐封扫而不是看 sessions 表:**参与方是随往来增长的**。会话建立时
// 只有发件人与收件人一封抄送、一次转发都会带进新的人。sessions 表里只有
// from_agent 一个名字,回答不了「这条线索上现在有谁」。
//
// 排序按首次出现顺序created_at。这让主收件人稳定排在抄送方之前
// 模型据此判断「谁是负责人、谁是配合方」——按名字排序会丢掉这个信息。
func SessionParticipants(ctx context.Context, sessionID uuid.UUID) ([]Participant, error) {
rows, err := db.DB.QueryContext(ctx, `
SELECT from_name, COALESCE(from_workspace,''),
to_name, COALESCE(to_workspace,''),
cc_list
FROM mails
WHERE session_id = $1
ORDER BY created_at ASC, mail_id ASC
`, sessionID)
if err != nil {
return nil, err
}
defer rows.Close()
type acc struct {
p Participant
roles map[string]bool
order int
}
seen := map[string]*acc{}
next := 0
// note 记录一次「某人以某身份出现」。
//
// path 只在**当前为空且新值非空**时补写:同一个人可能在不同邮件里带不同
// path先被抄送到 /a后被主发到 /b。保留首个非空值而不是最后一个
// 与排序口径一致(首次出现顺序),也避免一封转发把地址改指到别处。
note := func(name, path, role string, isSender bool) {
if name == "" {
return
}
a, ok := seen[name]
if !ok {
a = &acc{
p: Participant{Name: name, Path: path},
roles: map[string]bool{},
order: next,
}
next++
seen[name] = a
}
if a.p.Path == "" && path != "" {
a.p.Path = path
}
a.roles[role] = true
if isSender {
a.p.MailCount++
}
}
for rows.Next() {
var fromName, fromWS, toName, toWS string
var ccRaw []byte
if err := rows.Scan(&fromName, &fromWS, &toName, &toWS, &ccRaw); err != nil {
return nil, err
}
// **发件人一侧不取 from_workspace 当 path。** Agent 回信时那一列存的是
// Agent 名而不是路径历史遗留FindOrCreateDefaultSession 的注释里也提到
// 同一个坑)。拿它拼地址会得到 `dsh@dsh.alias` 这种投不出去的东西。
note(fromName, "", "from", true)
note(toName, toWS, "to", false)
if len(ccRaw) > 0 {
var cc []models.Address
if json.Unmarshal(ccRaw, &cc) == nil {
for _, c := range cc {
note(c.Name, c.Path, "cc", false)
}
}
}
}
if err := rows.Err(); err != nil {
return nil, err
}
out := make([]Participant, 0, len(seen))
for _, a := range seen {
a.p.Roles = sortedKeys(a.roles)
out = append(out, a.p)
}
sort.Slice(out, func(i, j int) bool {
return seen[out[i].Name].order < seen[out[j].Name].order
})
return out, nil
}
// sortedKeys 给出稳定顺序的角色列表。
// map 迭代顺序随机,不排序的话同一条会话每次返回的 roles 顺序都不同,
// 插件侧做 diff 或缓存时会误判为「参与方变了」。
func sortedKeys(m map[string]bool) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}