From f0c7e5cbbce81bcf476009226db1037ee6231d79 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 21 Sep 2026 06:32:43 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5:=20=E9=82=A3=E6=9D=A1=2040=20?= =?UTF-8?q?=E8=A1=8C=E5=9B=9E=E5=A1=AB**=E5=8F=AA=E6=B8=85=20(b)=20?= =?UTF-8?q?=E9=82=A3=E4=B8=80=E5=8D=8A**=20=E2=80=94=E2=80=94=20(a)=20?= =?UTF-8?q?=E7=95=99=E4=B8=8B=E7=9A=84=20116=20=E8=A1=8C=E5=AE=83=E4=B8=80?= =?UTF-8?q?=E6=9D=A1=E9=83=BD=E9=80=89=E4=B8=8D=E5=88=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 商定的回填 SQL 带 `WHERE m.status='read'`,只覆盖**冗余列已经是 read** 的(成因 (b))。 成因 (a)(`read_mail` 不标已读)留下的是 **`mails.status` 仍 unread + `mail_reads` 无行** ⇒ 那个 SQL **一条都选不到**。 ## 判据:不靠"我觉得没读过",靠可观测的因果 **收件人回了这封信 ⇒ 它必然读过** ⇒ 此时若无 `mail_reads` 行,则两条记录都没记上 (父信的 `to_name` 恰是子信的 `from_name`)。实测 **248 封**: read 28 ✅ 回填选得到(属于那 40 行的一部分) unread 116 ❌ 选不到 archived 104 ❌ 选不到 实例:`fd375458`(dsh→pi)—— **pi 回了 `494b29e4`**(`parent_mail_id` 指向它)⇒ pi 读过; 但该信 `mail_reads` 零行、`mails.status` 仍是 `unread`。 ## 结论 落那 40 行只清掉 **(b)**;**(a) 留下的 116 行原样留着**。 不要把它写成"补完历史缺口"—— 它补的是**冗余列与权威列之间**的差, 不是**"读过"与"没记上"之间**的差。后者要另立一条判据。 (判据口径说明:`收件人回过`是**充分**证据,不是全部 ⇒ 真实漏记量 **≥ 248**。) ★ 与本节开头"计数对了不等于账记上了"是**同一个形状**:这次是"补了 40 行"≠"账平了"。 --- docs/API.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/API.md b/docs/API.md index 52610e5..f17daca 100644 --- a/docs/API.md +++ b/docs/API.md @@ -496,6 +496,51 @@ curl -X POST {host}/api/v1/mail/read -H "Authorization: Bearer $AGENT_KEY" ⇒ 判"还欠多少"不能只数**总数**,要数**成员集合**(或直接看部署了没有)。 **一个稳定的计数可以掩盖一个持续在发生的错误。** +- ★★ **回填 SQL 的覆盖面比"40 行"这个数小得多(2026-09-21 量到)**。 + 现在商定的回填是: + + ```sql + INSERT INTO mail_reads (mail_id, reader_name) + SELECT m.mail_id, m.to_name FROM mails m + WHERE m.status='read' AND m.to_name <> '' + AND NOT EXISTS (SELECT 1 FROM mail_reads r + WHERE r.mail_id=m.mail_id AND r.reader_name=m.to_name); + ``` + + 它的 `WHERE m.status='read'` 只覆盖**冗余列已经是 `read`** 的那些 —— 也就是成因 **(b)** + (`markReadFor` 漏写权威列)。但成因 **(a)**(`read_mail` 不标已读)留下的是 + **`mails.status` 仍是 `unread` + `mail_reads` 无行** —— **这个 SQL 一条都选不到。** + + **判据(不靠"我觉得没读过",而靠可观测的因果)**:收件人**回了这封信** + ⇒ 它必然读过 ⇒ 若此时无 `mail_reads` 行,则两条记录都没记上: + + ```sql + -- 父信的收件人,恰是子信的 from_name + SELECT m.mail_id, m.to_name, m.status FROM mails m + WHERE m.to_name <> '' + AND EXISTS (SELECT 1 FROM mails ch + WHERE ch.parent_mail_id = m.mail_id AND ch.from_name = m.to_name) + AND NOT EXISTS (SELECT 1 FROM mail_reads r + WHERE r.mail_id=m.mail_id AND r.reader_name=m.to_name); + ``` + + 实测 **248 封**,按 `mails.status` 拆: + + | `mails.status` | 封数 | 回填 SQL 选得到吗 | + |---|---|---| + | `read` | 28 | ✅ 能(这部分属于那 40 行) | + | `unread` | 116 | ❌ **选不到** | + | `archived` | 104 | ❌ 选不到(且 `unreadFor` 也把 archived 当"不算未读") | + + 实例(可直接核对):`fd375458` 是 dsh→pi,**pi 回了 `494b29e4`**(`parent_mail_id` 指向它) + ⇒ pi 读过;但该信的 `mail_reads` **零行**、`mails.status` 仍是 **`unread`**。 + + ⇒ **结论:落那条 40 行的回填只清掉 (b) 那一半;(a) 留下的 116 行原样留着。** + 不要把它写成"补完历史缺口"——它补的是**冗余列与权威列之间**的差, + 不是**"读过"与"没记上"之间**的差。后者要另立一条(按上面的判据重算,且必须说明 + "收件人回过"只是**充分**证据,真实漏记量 ≥ 248)。 + **"补完 40 行"与"历史账平了"是两件事** —— 这正是本节开头那条"A 对不代表 B 对"的同一个形状。 + ### 每个 Agent 可用的模型范围 ```