fix(web): 地址显示按「人 / Agent」分维度 —— 别名跟 Agent 走,人只显示名字
## 症状
单封邮件的元信息三行都不对(生产实测那封 12:12:12):
发件 jianf.邮件驱动·多智能体协作平台-完整设计文档-一、项目概述-11-项目定位
收件 pi@/home/program/agentmail
抄送 pi@/home/program/agentmail.new
人指定的是「投进 pi 的那条会话」,而界面把会话别名拼给了**发件人**。
## 三处错
**1. 别名拼错了一方。** `name@path.session` 三段才唯一确定「哪个 Agent、
在哪个目录、哪条线索」—— 别名必须跟 Agent 走。拼给发件人之后收件人变成
`pi@/home/program/agentmail`,那指向**默认会话**而不是人指定的那条。
**2. 人不该有目录和会话位。** 人没有工作目录,发给人就是进收件箱。
`jianf.某会话` 是把 Agent 的三维语义硬套在人身上,而且因为 from_workspace
为空,拼出来的形态连 ParseAddress 都还原不了 —— 没有 `@` 时整串被当成
**名字**(实测 name="jianf.某会话别名"),投递必然 404。
**3. 抄送残留 `.new`。** 它是一次性动作,建完会话就失效;留着会让人以为
再发一次还能投进同一条会话,实际会开出第三条。
## 修法
`identityAddress` → `participantAddress(name, workspace, alias)`,
判据是有没有 workspace:
Agent → pi@/home/program/agentmail.日程提醒:… 三段齐全
人 → jianf 裸名字
`ccAddress` 按同一判据分流;`.new` 换成当前会话别名。
六处手工拼接(MailView / MailList ×2 / ThreadView)统一走这两个函数。
## 顺带修掉 `dsh@dsh`
改的时候实测发现:**`mails.from_workspace` 对 Agent 存的是 Agent 名而不是
路径**(历史遗留,见 db/migrate.go 里 sessions.workspace 的注释)。
拿它当路径拼,Agent 发来的信显示成 `dsh@dsh`。
会话的 workspace 才是权威来源 → `models.Mail` 新增 `SessionWorkspace`,
六处查询补 `s.workspace`:GetMailByID / ListInbox / GetSessionMails /
GetSessionMailByID / ListSentBy / threadCols。
## formatAddress 与后端对齐
第一版我改成「path 为空时舍弃 session 返回裸名字」,对着后端 ParseAddress
跑了一遍才发现搞反了 —— **正确形态是保留 `@`**:
jianf@.任务 → name=jianf path="" session=任务 ✓
jianf.任务 → name="jianf.任务" ✗
现在两端六个 case 逐例一致(这个分支只在内部逻辑上用得到;
展示一律走 participantAddress,人根本不带会话位)。
## 取舍
列表行与对话树节点**不带会话位**:列表的分组头已单独显示别名,
树的每个节点都在同一条线索上 —— 重复无信息量,而 92 字节的别名会把那行挤没。
## homeagent 日程工具的两个修复(同批)
**查询串手拼吃掉了时区。** RFC3339 的 `+08:00` 里那个 `+` 在查询串里正是
空格的转义形式,服务端 ParseQuery 还原成空格 → time.Parse 失败 →
AgentListCalendarEvents **静默退回默认区间**(不报错)。表现为「明明有日程
却说一条都没有」。改走 url.Values.Encode()。
**默认窗口 3 个月太窄。** yearly / lunar_yearly 的下一次触发随时落在窗口外,
模型问「我建过什么」得到空结果,然后照着空结果再建一条重复的。改成 14 个月。
空结果的话术也从「你还没有建过日程」改成说出实际查询区间 —— 前者在窗口外
有事件时是假话。
## 验收
- web 182 例(replyTarget 24 → 46);tsc 无错;Gateway 7 包全过
- 新增 test/manual/addr-verify.mjs:真渲染两个方向都验过
人 → Agent:jianf / pi@/home/program/agentmail.日程提醒:…
Agent → 人:dsh@/home/program/agentmail.查看工程与插件适配指南 / jianf
判据含「Agent 的 path 必须是真路径而不是 Agent 名」(锁 dsh@dsh 那个 bug)
This commit is contained in:
@ -9,13 +9,13 @@ import (
|
||||
|
||||
// Agent 代表一个已注册的 Agent 实例
|
||||
type Agent struct {
|
||||
ID uuid.UUID `json:"agent_id"`
|
||||
Name string `json:"agent_name"`
|
||||
Secret string `json:"-"`
|
||||
HostURL string `json:"host_url"`
|
||||
ID uuid.UUID `json:"agent_id"`
|
||||
Name string `json:"agent_name"`
|
||||
Secret string `json:"-"`
|
||||
HostURL string `json:"host_url"`
|
||||
Workspaces []Workspace `json:"workspaces"`
|
||||
Platform string `json:"platform"`
|
||||
Status string `json:"status"`
|
||||
Platform string `json:"platform"`
|
||||
Status string `json:"status"`
|
||||
// DefaultRounds 是派给该 Agent 的新任务默认多少个来回(0 = 不限)。
|
||||
// 真正的额度在每条会话上(sessions.max_rounds),这里只是默认值。
|
||||
DefaultRounds int `json:"default_rounds"`
|
||||
@ -122,25 +122,38 @@ func (u User) CanUsePath(path string) bool {
|
||||
|
||||
// Mail 是会话中的一封邮件
|
||||
type Mail struct {
|
||||
ID uuid.UUID `json:"mail_id"`
|
||||
SessionID uuid.UUID `json:"session_id"`
|
||||
ParentMailID *uuid.UUID `json:"parent_mail_id"`
|
||||
FromName string `json:"from_name"`
|
||||
FromWorkspace string `json:"from_workspace"`
|
||||
ToName string `json:"to_name"`
|
||||
ToWorkspace string `json:"to_workspace"`
|
||||
CCList []Address `json:"cc_list"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
MailType string `json:"mail_type"`
|
||||
PermOptions []string `json:"permission_options,omitempty"`
|
||||
PermResult string `json:"permission_result,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
HopLimit int `json:"hop_limit"`
|
||||
ID uuid.UUID `json:"mail_id"`
|
||||
SessionID uuid.UUID `json:"session_id"`
|
||||
ParentMailID *uuid.UUID `json:"parent_mail_id"`
|
||||
FromName string `json:"from_name"`
|
||||
FromWorkspace string `json:"from_workspace"`
|
||||
ToName string `json:"to_name"`
|
||||
ToWorkspace string `json:"to_workspace"`
|
||||
CCList []Address `json:"cc_list"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
MailType string `json:"mail_type"`
|
||||
PermOptions []string `json:"permission_options,omitempty"`
|
||||
PermResult string `json:"permission_result,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
HopLimit int `json:"hop_limit"`
|
||||
|
||||
SessionAlias string `json:"session_alias,omitempty"`
|
||||
BodyPreview string `json:"body_preview,omitempty"`
|
||||
|
||||
// SessionWorkspace 是**这条会话**的工作目录(sessions.workspace)。
|
||||
//
|
||||
// 为什么不能用 FromWorkspace / ToWorkspace 代替:
|
||||
// - 人 → Agent:to_workspace 是真路径,from_workspace 为空(人没有工作目录)
|
||||
// - Agent → 人:to_workspace 为空,而 **from_workspace 存的是 Agent 名
|
||||
// 而不是路径**(历史遗留,见 db/migrate.go 的 sessions.workspace 注释)
|
||||
//
|
||||
// 于是「Agent 发来的这封信,那个 Agent 在哪个目录干活」只能从会话上取 ——
|
||||
// 前端要靠它拼出 `name@path.alias` 这个可投递地址(界面上曾显示成
|
||||
// `dsh@dsh`,就是拿 from_workspace 当路径拼出来的)。
|
||||
SessionWorkspace string `json:"session_workspace,omitempty"`
|
||||
|
||||
BodyPreview string `json:"body_preview,omitempty"`
|
||||
|
||||
// Attachments 仅在读取单封邮件/会话线程时填充;列表接口为省带宽留空
|
||||
Attachments []Attachment `json:"attachments,omitempty"`
|
||||
@ -234,7 +247,7 @@ func TokenHint(token string) string {
|
||||
// Attachment 是一封邮件的附件元数据。文件内容存磁盘,按 sha256 内容寻址。
|
||||
//
|
||||
// MailID 为空表示「已上传、尚未挂到邮件上」:上传与发信是两步操作
|
||||
//(Agent 侧工具走 JSON,无法在发信请求里带 multipart),中间态必须允许存在。
|
||||
// (Agent 侧工具走 JSON,无法在发信请求里带 multipart),中间态必须允许存在。
|
||||
type Attachment struct {
|
||||
ID uuid.UUID `json:"attachment_id"`
|
||||
MailID *uuid.UUID `json:"mail_id"`
|
||||
|
||||
@ -341,14 +341,14 @@ func GetMailByID(ctx context.Context, id uuid.UUID) (*models.Mail, error) {
|
||||
`SELECT m.mail_id, m.session_id, m.parent_mail_id,
|
||||
m.from_name, m.from_workspace, m.to_name, m.to_workspace,
|
||||
m.cc_list, m.subject, m.body, m.mail_type, COALESCE(m.permission_result,'') AS permission_result,
|
||||
m.status, m.created_at, s.session_alias, m.rename_alias, m.rename_reason
|
||||
m.status, m.created_at, s.session_alias, s.workspace, m.rename_alias, m.rename_reason
|
||||
FROM mails m
|
||||
JOIN sessions s ON m.session_id = s.session_id
|
||||
WHERE m.mail_id = $1`, id,
|
||||
).Scan(&m.ID, &m.SessionID, &m.ParentMailID,
|
||||
&m.FromName, &m.FromWorkspace, &m.ToName, &m.ToWorkspace,
|
||||
&ccJSON, &m.Subject, &m.Body, &m.MailType, &m.PermResult,
|
||||
&m.Status, &m.CreatedAt, &alias, &renameAlias, &renameReason)
|
||||
&m.Status, &m.CreatedAt, &alias, &m.SessionWorkspace, &renameAlias, &renameReason)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -381,7 +381,7 @@ func ListInbox(ctx context.Context, agentName, status string, limit int) ([]mode
|
||||
q := `SELECT m.mail_id, m.session_id, m.parent_mail_id,
|
||||
m.from_name, m.from_workspace, m.to_name, m.to_workspace,
|
||||
m.cc_list, m.subject, m.body, m.mail_type, COALESCE(m.permission_result,'') AS permission_result,
|
||||
m.status, m.created_at, s.session_alias
|
||||
m.status, m.created_at, s.session_alias, s.workspace
|
||||
FROM mails m
|
||||
JOIN sessions s ON m.session_id = s.session_id
|
||||
WHERE (m.to_name = $1 OR ` + db.CCHas("m.cc_list", 1) + `)
|
||||
@ -410,7 +410,7 @@ func ListInbox(ctx context.Context, agentName, status string, limit int) ([]mode
|
||||
if err := rows.Scan(&m.ID, &m.SessionID, &m.ParentMailID,
|
||||
&m.FromName, &m.FromWorkspace, &m.ToName, &m.ToWorkspace,
|
||||
&ccJSON, &m.Subject, &m.Body, &m.MailType, &m.PermResult,
|
||||
&m.Status, &m.CreatedAt, &alias); err != nil {
|
||||
&m.Status, &m.CreatedAt, &alias, &m.SessionWorkspace); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ccJSON) > 0 {
|
||||
@ -451,7 +451,7 @@ func GetSessionMails(ctx context.Context, sessionID uuid.UUID) ([]models.Mail, e
|
||||
`SELECT m.mail_id, m.session_id, m.parent_mail_id,
|
||||
m.from_name, m.from_workspace, m.to_name, m.to_workspace,
|
||||
m.cc_list, m.subject, m.body, m.mail_type, COALESCE(m.permission_result,'') AS permission_result,
|
||||
m.status, m.created_at, s.session_alias
|
||||
m.status, m.created_at, s.session_alias, s.workspace
|
||||
FROM mails m
|
||||
JOIN sessions s ON m.session_id = s.session_id
|
||||
WHERE m.session_id = $1
|
||||
@ -469,7 +469,7 @@ func GetSessionMails(ctx context.Context, sessionID uuid.UUID) ([]models.Mail, e
|
||||
if err := rows.Scan(&m.ID, &m.SessionID, &m.ParentMailID,
|
||||
&m.FromName, &m.FromWorkspace, &m.ToName, &m.ToWorkspace,
|
||||
&ccJSON, &m.Subject, &m.Body, &m.MailType, &m.PermResult,
|
||||
&m.Status, &m.CreatedAt, &alias); err != nil {
|
||||
&m.Status, &m.CreatedAt, &alias, &m.SessionWorkspace); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ccJSON) > 0 {
|
||||
@ -570,13 +570,13 @@ func GetSessionMailByID(ctx context.Context, sessionID, mailID uuid.UUID) (*mode
|
||||
`SELECT m.mail_id, m.session_id, m.parent_mail_id,
|
||||
m.from_name, m.from_workspace, m.to_name, m.to_workspace,
|
||||
m.cc_list, m.subject, m.body, m.mail_type, COALESCE(m.permission_result,'') AS permission_result,
|
||||
m.status, m.created_at, s.session_alias
|
||||
m.status, m.created_at, s.session_alias, s.workspace
|
||||
FROM mails m JOIN sessions s ON m.session_id = s.session_id
|
||||
WHERE m.session_id = $1 AND m.mail_id = $2`, sessionID, mailID,
|
||||
).Scan(&m.ID, &m.SessionID, &m.ParentMailID,
|
||||
&m.FromName, &m.FromWorkspace, &m.ToName, &m.ToWorkspace,
|
||||
&ccJSON, &m.Subject, &m.Body, &m.MailType, &m.PermResult,
|
||||
&m.Status, &m.CreatedAt, &alias)
|
||||
&m.Status, &m.CreatedAt, &alias, &m.SessionWorkspace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1036,7 +1036,7 @@ func ListSentBy(ctx context.Context, fromName string, limit int) ([]models.Mail,
|
||||
SELECT m.mail_id, m.session_id, m.parent_mail_id,
|
||||
m.from_name, m.from_workspace, m.to_name, m.to_workspace,
|
||||
m.cc_list, m.subject, m.body, m.mail_type, COALESCE(m.permission_result,'') AS permission_result,
|
||||
m.status, m.created_at, s.session_alias
|
||||
m.status, m.created_at, s.session_alias, s.workspace
|
||||
FROM mails m
|
||||
JOIN sessions s ON m.session_id = s.session_id
|
||||
WHERE m.from_name = $1 AND s.status <> 'archived'
|
||||
@ -1056,7 +1056,7 @@ func ListSentBy(ctx context.Context, fromName string, limit int) ([]models.Mail,
|
||||
if err := rows.Scan(&m.ID, &m.SessionID, &m.ParentMailID,
|
||||
&m.FromName, &m.FromWorkspace, &m.ToName, &m.ToWorkspace,
|
||||
&ccJSON, &m.Subject, &m.Body, &m.MailType, &m.PermResult,
|
||||
&m.Status, &m.CreatedAt, &alias); err != nil {
|
||||
&m.Status, &m.CreatedAt, &alias, &m.SessionWorkspace); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ccJSON) > 0 {
|
||||
|
||||
@ -50,7 +50,7 @@ const threadCols = `m.mail_id, m.session_id, m.parent_mail_id,
|
||||
m.from_name, m.from_workspace, m.to_name, m.to_workspace,
|
||||
m.cc_list, m.subject, m.body, m.mail_type,
|
||||
COALESCE(m.permission_result,'') AS permission_result,
|
||||
m.status, m.created_at, s.session_alias,
|
||||
m.status, m.created_at, s.session_alias, s.workspace,
|
||||
(SELECT COUNT(*) FROM attachments a WHERE a.mail_id = m.mail_id) AS attach_count`
|
||||
|
||||
// ThreadRootOf 沿 parent_mail_id 上溯到线索的根,返回根的 mail_id 与锚点到根的层数。
|
||||
@ -202,7 +202,7 @@ func scanTreeRows(rows interface {
|
||||
if err := rows.Scan(&t.ID, &t.SessionID, &t.ParentMailID,
|
||||
&t.FromName, &t.FromWorkspace, &t.ToName, &t.ToWorkspace,
|
||||
&ccJSON, &t.Subject, &t.Body, &t.MailType, &t.PermResult,
|
||||
&t.Status, &t.CreatedAt, &alias, &t.AttachmentCount, &lvl); err != nil {
|
||||
&t.Status, &t.CreatedAt, &alias, &t.SessionWorkspace, &t.AttachmentCount, &lvl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ccJSON) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user