From 7ff20feffe7c4d3ac338f589167e49fe8fcbc104 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 15 Sep 2026 09:36:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(forward):=20=E8=BD=AC=E5=8F=91=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E9=87=8C=E7=9A=84=E3=80=8C=E6=97=B6=E9=97=B4=E3=80=8D?= =?UTF-8?q?=E6=8C=89=E6=9C=AC=E5=9C=B0=E6=97=B6=E5=8C=BA=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=EF=BC=88=E5=8E=9F=E5=85=88=E6=98=AF=20UTC=20=E5=8E=9F=E6=A0=B7?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户(转发 zcode 的信给我):「它说的它发的这个邮件你收到了吗?它说没有收到回复……」—— 问题不在那封信,而在它引用的时间:用户看到的「2026-09-15 01:23:19」其实是 UTC, 本地时间应为 09:23:19(本机 UTC+8)。 根因:quoteBody 用 `m.CreatedAt.Format(...)` 直接格式化(库里存 UTC),而界面各处都转本地; 同一族的正确写法在 scheduler/calendar.go 里(`e.EventTime.Local().Format(...)`)。 ★ 顺带发现一条**旧断言一直在保护这个 bug**:TestQuoteBodyPrefixesEveryLine 里写死了 「2026-09-02 10:30:00」(fixture 是 time.UTC)——已改为按本地计算,不再写死字面量 (与前几天 nav-merge.test.mjs 那条同一形态:断言钉着错的意图)。 判据:新增 TestQuoteBodyTimeIsLocal(引用时间必须等于 CreateAt.Local() 渲染,且不得等于 UTC 原样)。变异:去掉 .Local() → 2 条判据变红(新加的 + 改过的旧断言);复原 → 包内全绿。 注:该判据在 TZ=UTC 的机器上是空转的(本地==UTC),本机 UTC+8 会真红。 ▲ 部署未完成:/opt/agentmail 现在对我不可写(touch 都被拒;uid/能力问题,08:57 之后出现), 加上一个 08:57 留下的陈旧 /opt/agentmail/.deploy.lock 挡在 redeploy-gateway.sh 前面。 需要能写那里的人跑:rm -f /opt/agentmail/.deploy.lock && bash deploy/redeploy-gateway.sh --- server/internal/handler/forward.go | 5 ++++- server/internal/handler/forward_test.go | 29 ++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/server/internal/handler/forward.go b/server/internal/handler/forward.go index 7086ba9..e99f46d 100644 --- a/server/internal/handler/forward.go +++ b/server/internal/handler/forward.go @@ -45,7 +45,10 @@ func quoteBody(m *models.Mail) string { } b.WriteString("\n") b.WriteString(fmt.Sprintf("> **主题** %s\n", m.Subject)) - b.WriteString(fmt.Sprintf("> **时间** %s\n", m.CreatedAt.Format("2006-01-02 15:04:05"))) + // ★ 2026-09-15 用户报:转发引用里显示「2026-09-15 01:23:19」,而实际是 09:23:19 —— + // 原文时间按 **UTC** 原样格式化了。库里存的是 UTC,界面各处都转本地,只有这一处漏了 + // (同一族的写法在 scheduler/calendar.go 里是对的:e.EventTime.Local().Format(...))。 + b.WriteString(fmt.Sprintf("> **时间** %s\n", m.CreatedAt.Local().Format("2006-01-02 15:04:05"))) if len(m.CCList) > 0 { names := make([]string, 0, len(m.CCList)) for _, c := range m.CCList { diff --git a/server/internal/handler/forward_test.go b/server/internal/handler/forward_test.go index 384b67d..2bc2e10 100644 --- a/server/internal/handler/forward_test.go +++ b/server/internal/handler/forward_test.go @@ -51,7 +51,9 @@ func TestQuoteBodyPrefixesEveryLine(t *testing.T) { } // 元信息必须齐全,否则收件人不知道这封转发的来路 - for _, want := range []string{"opencode@/root", "巡检结果", "2026-09-02 10:30:00", "pi@root.new"} { + // ★ 2026-09-15:这里原先写死 "2026-09-02 10:30:00"(UTC 原样),而 fixture 是 time.UTC —— + // 等于这条断言一直在**保护那个 bug**(引用块显示 UTC 时间)。时间一项按本地算,不写死字面量。 + for _, want := range []string{"opencode@/root", "巡检结果", m.CreatedAt.Local().Format("2006-01-02 15:04:05"), "pi@root.new"} { if !strings.Contains(out, want) { t.Errorf("引用块缺少 %q\n%s", want, out) } @@ -75,3 +77,28 @@ func TestQuoteBodyOmitsEmptyCC(t *testing.T) { t.Error("无抄送时不应出现「抄送」行") } } + +// TestQuoteBodyTimeIsLocal —— 转发引用里的「时间」必须是**本地**时间,不是 UTC 原样。 +// +// 2026-09-15 用户报:引用块显示 01:23:19,而实际发生时间是 09:23:19(本机 UTC+8)。 +// 库里存 UTC、界面各处都转本地,只有 quoteBody 漏了 .Local()。 +// +// ★ 这条判据在 **TZ=UTC 的机器上是空转的**(本地 == UTC,两种写法同结果)—— +// 本机是 UTC+8,所以这里真会红。CI 若在 UTC 下跑,它是"只能验形态"的那一类, +// 形态判据另有一条:不得与 UTC 原样相等(当两者不同时)。 +func TestQuoteBodyTimeIsLocal(t *testing.T) { + // 取一个"本地与 UTC 不同"的瞬间来测(否则本判据无从区分) + m := &models.Mail{ + FromName: "zcode", + Subject: "时间判定", + CreatedAt: time.Date(2026, 9, 15, 1, 23, 19, 0, time.UTC), + } + 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) + } + if u := m.CreatedAt.UTC().Format("2006-01-02 15:04:05"); u != want && strings.Contains(got, "> **时间** "+u) { + t.Fatalf("引用里出现的是 UTC 原样 %q(应为本地 %q)", u, want) + } +}