diff --git a/server/internal/handler/forward.go b/server/internal/handler/forward.go index e99f46d..d6ae255 100644 --- a/server/internal/handler/forward.go +++ b/server/internal/handler/forward.go @@ -36,13 +36,10 @@ type forwardRequest struct { // quoteBody 把原文渲染为 Markdown 引用块。 // 逐行加 "> " 而不是整段包裹:原文本身可能含代码块与列表, // 只有逐行前缀才能在任何 Markdown 渲染器里保持引用语义。 -func quoteBody(m *models.Mail) string { +func quoteBody(m *models.Mail, sessionWorkspace, sessionAlias string) string { var b strings.Builder b.WriteString("---\n\n") - b.WriteString(fmt.Sprintf("> **转发自** %s", m.FromName)) - if m.FromWorkspace != "" { - b.WriteString("@" + m.FromWorkspace) - } + b.WriteString(fmt.Sprintf("> **转发自** %s", senderAddress(m, sessionWorkspace, sessionAlias))) b.WriteString("\n") b.WriteString(fmt.Sprintf("> **主题** %s\n", m.Subject)) // ★ 2026-09-15 用户报:转发引用里显示「2026-09-15 01:23:19」,而实际是 09:23:19 —— @@ -63,6 +60,39 @@ func quoteBody(m *models.Mail) string { return b.String() } + +// senderAddress 拼出转发引用里那个「转发自」地址。 +// +// ★ 2026-09-15 用户报:「这个 zcode@zcode 转发生成的地址绝对有问题,原本是 zcode@/home…」 +// +// 两个原因叠在一起: +// ① **发信路径给 Agent 存 from_workspace 存的是 agent 名**,不是路径 +// (实测:zcode 近两天 10 封全是 'zcode',homeagent 'homeagent',opencode 'opencode'; +// pi/dsh 是混的:既有真实路径也有自己的名字)—— 这是历史遗留的写入侧问题; +// ② 引用块又无脑拼 `name@from_workspace` ⇒ 两者一乘就是 `zcode@zcode`, +// 而**那永远不是一个地址**(地址是 name@path.session)。 +// +// 这里改成用**会话**的真实 workspace/alias 拼(`SessionWorkspaceOf`/`SessionAliasOf`, +// 读不到就退化成信上记的值),并且**一旦发现 path 等于 name 就当作"不知道"丢掉** —— +// 宁可渲染 `zcode`,也不渲染一个看着像地址其实不是的东西。 +func senderAddress(m *models.Mail, sessionWorkspace, sessionAlias string) string { + path := sessionWorkspace + if path == "" { + path = m.FromWorkspace + } + if path == m.FromName { + path = "" // agent 名被当成路径存下来的遗留数据:那不是地址 + } + addr := m.FromName + if path != "" || sessionAlias != "" { + addr += "@" + path + if sessionAlias != "" { + addr += "." + sessionAlias + } + } + return addr +} + // forwardSubject 生成转发主题,避免 "Fwd: Fwd: Fwd:" 无限叠加。 func forwardSubject(custom, original string) string { if s := strings.TrimSpace(custom); s != "" { @@ -167,7 +197,7 @@ func doForward(w http.ResponseWriter, r *http.Request, mailID uuid.UUID, actor, _ = repo.SetSessionOwner(r.Context(), sessionID, user.ID) } - body := quoteBody(src) + body := quoteBody(src, repo.SessionWorkspaceOf(r.Context(), src.SessionID), repo.SessionAliasOf(r.Context(), src.SessionID)) if c := strings.TrimSpace(req.Comment); c != "" { body = c + "\n\n" + body } diff --git a/server/internal/handler/forward_test.go b/server/internal/handler/forward_test.go index 2bc2e10..7a9f3ca 100644 --- a/server/internal/handler/forward_test.go +++ b/server/internal/handler/forward_test.go @@ -39,7 +39,7 @@ func TestQuoteBodyPrefixesEveryLine(t *testing.T) { }, } - out := quoteBody(m) + out := quoteBody(m, "", "") // 这条用例的 fixture 里 from_workspace 本身是真路径,走退化路径 for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") { if line == "---" || line == "" { @@ -73,7 +73,7 @@ func TestQuoteBodyOmitsEmptyCC(t *testing.T) { Body: "y", CreatedAt: time.Now(), } - if strings.Contains(quoteBody(m), "抄送") { + if strings.Contains(quoteBody(m, "", ""), "抄送") { t.Error("无抄送时不应出现「抄送」行") } } @@ -93,7 +93,7 @@ func TestQuoteBodyTimeIsLocal(t *testing.T) { Subject: "时间判定", CreatedAt: time.Date(2026, 9, 15, 1, 23, 19, 0, time.UTC), } - got := quoteBody(m) + got := quoteBody(m, "", "") want := m.CreatedAt.Local().Format("2006-01-02 15:04:05") if !strings.Contains(got, "> **时间** "+want) { t.Fatalf("引用里的时间不是本地时间:想要 %q,实际渲染:\n%s", want, got) @@ -102,3 +102,30 @@ func TestQuoteBodyTimeIsLocal(t *testing.T) { t.Fatalf("引用里出现的是 UTC 原样 %q(应为本地 %q)", u, want) } } + +// TestQuoteBodySenderAddressNeverNameAtName —— 转发引用里的「转发自」必须是个**地址**。 +// +// 2026-09-15 用户报:「这个 zcode@zcode 转发生成的地址绝对有问题,原本是 zcode@/home…」 +// 成因:发信路径给 Agent 存的 from_workspace 是 **agent 名**(zcode/homeagent/opencode 全如此), +// 而引用块按 `name@from_workspace` 拼 ⇒ zcode@zcode。 +// 判据钉两条:① 用**会话**的 workspace/alias 拼出 `name@path.session`; +// ② path 与 name 相同时也**不许**渲染成 name@name(宁可只写名字)。 +func TestQuoteBodySenderAddressNeverNameAtName(t *testing.T) { + legacy := &models.Mail{FromName: "zcode", FromWorkspace: "zcode", Subject: "打个招呼", CreatedAt: time.Now()} + got := quoteBody(legacy, "/home", "zcode-打个招呼") + if !strings.Contains(got, "> **转发自** zcode@/home.zcode-打个招呼") { + t.Fatalf("转发自应为 name@path.session,实际:\n%s", got) + } + if strings.Contains(got, "zcode@zcode") { + t.Fatalf("又拼出了 name@name(那不是地址):\n%s", got) + } + // 退化路径:没有会话信息、且 from_workspace 就是 agent 名 → 只写名字,不编地址 + if got := quoteBody(legacy, "", ""); strings.Contains(got, "zcode@zcode") { + t.Fatalf("退化时不该出现 name@name:\n%s", got) + } + // 空 path 但别名在:平台形式是 name@.alias + pure := &models.Mail{FromName: "jianf", Subject: "x", CreatedAt: time.Now()} + if got := quoteBody(pure, "", "会话别名"); !strings.Contains(got, "> **转发自** jianf@.会话别名") { + t.Fatalf("空 path 时应为 name@.alias,实际:\n%s", got) + } +}