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:
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -103,29 +104,58 @@ func (p *Plugin) handleCreateSchedule(args map[string]interface{}) (interface{},
|
||||
}
|
||||
|
||||
// handleListSchedules 列出自己建的日程。
|
||||
//
|
||||
// 查询串必须走 url.Values 编码,**不能手拼**。
|
||||
//
|
||||
// RFC3339 的时区偏移带一个 `+`(2026-12-11T09:00:00+08:00),而 `+` 在查询串里
|
||||
// 正是空格的转义形式:手拼进 URL 后服务端 ParseQuery 把它还原成空格,
|
||||
// time.Parse(RFC3339) 随即失败 —— 而 AgentListCalendarEvents 解析失败时是
|
||||
// **静默退回默认区间**(now-24h ~ now+3月),不报错。
|
||||
// 于是这里传的 from/to 全部无效,表现为「明明有日程却说一条都没有」。
|
||||
// 真实踩过一次:先怀疑了数据库、密钥和权限,最后才发现是这个加号。
|
||||
func (p *Plugin) handleListSchedules(args map[string]interface{}) (interface{}, error) {
|
||||
q := "?status=" + argStringOr(args, "status", "active")
|
||||
q := url.Values{}
|
||||
q.Set("status", argStringOr(args, "status", "active"))
|
||||
if v := strings.TrimSpace(argString(args, "from")); v != "" {
|
||||
if t, err := parseEventTime(v); err == nil {
|
||||
q += "&from=" + t.Format(time.RFC3339)
|
||||
q.Set("from", t.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
if v := strings.TrimSpace(argString(args, "to")); v != "" {
|
||||
if t, err := parseEventTime(v); err == nil {
|
||||
q += "&to=" + t.Format(time.RFC3339)
|
||||
q.Set("to", t.Format(time.RFC3339))
|
||||
}
|
||||
} else {
|
||||
// 默认往后 14 个月,而不是沿用服务端的 3 个月。
|
||||
//
|
||||
// 生日、纪念日这类 yearly / lunar_yearly 的下一次触发随时会落在
|
||||
// 3 个月之外。窗口太窄时模型问「我建过什么」得到的是空 ——
|
||||
// 然后它会照着空结果再建一条重复的。14 个月保证任何年度重复
|
||||
// 都至少露一次脸。
|
||||
q.Set("to", time.Now().AddDate(1, 2, 0).Format(time.RFC3339))
|
||||
}
|
||||
|
||||
var res struct {
|
||||
Events []scheduleEvent `json:"events"`
|
||||
ActiveLimit int `json:"active_limit"`
|
||||
}
|
||||
if err := p.get(p.gwURL+"/api/v1/agent/calendar/events"+q, &res); err != nil {
|
||||
if err := p.get(p.gwURL+"/api/v1/agent/calendar/events?"+q.Encode(), &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(res.Events) == 0 {
|
||||
return textResult("你还没有建过日程。用 create_schedule 建一条。"), nil
|
||||
// 不能说「你还没有建过日程」——窗口外或别的 status 下还有事件时
|
||||
// 这是假话,而模型会照着这句话去建一条重复的。
|
||||
// 把实际查询区间说出来,让它知道该往哪放宽。
|
||||
from := q.Get("from")
|
||||
if from == "" {
|
||||
from = "默认起点(昨天)"
|
||||
}
|
||||
return textResult(fmt.Sprintf(
|
||||
"这个区间内没有日程:%s ~ %s,status=%s。\n"+
|
||||
"想找更早/更远的请放宽 from/to,或用 status=all 看已暂停与已取消的。\n"+
|
||||
"确认真的没有再用 create_schedule 建 —— 别照着空结果建重复的。",
|
||||
from, q.Get("to"), q.Get("status"))), nil
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
Reference in New Issue
Block a user