From f9b849dcfcc9741aa7b976b748916c96d4bc9017 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 15:51:40 +0800 Subject: [PATCH] =?UTF-8?q?chore(deploy):=20=E6=B5=8B=E8=AF=95=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=B8=85=E7=90=86=E8=84=9A=E6=9C=AC=EF=BC=88=E5=8F=AF?= =?UTF-8?q?=E5=A4=8D=E7=8E=B0=E3=80=81=E9=BB=98=E8=AE=A4=E5=B9=B2=E8=B7=91?= =?UTF-8?q?=E3=80=81=E5=B8=A6=E5=A4=87=E4=BB=BD=E4=B8=8E=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户(2026-09-14):「清理一下」。库里积了 102 个会话 / 1038 封邮件, 其中 76 个是测试会话(gui-lab 专用测试 agent 发的 + drill-/e2e3-/冒烟/演练类)。 为什么不手敲 DELETE: ① 只看「收件箱里看得见的那几条」会漏掉大头 —— `ListInbox` 整体排除 `status='archived'`(repo.go),归档的测试会话在界面上根本不出现, 但在库里、在配额统计里都还在。所以按**来源 agent + 别名前缀**判定, 不按「看得见看不见」。 ② 直接 DELETE sessions 会留孤儿:mails/mail_reads/relayed_mails/attachments/ permission_requests 都指向它们(只有 attachments 有 ON DELETE CASCADE), 必须子表先删、且在同一个事务里。 ③ 附件文件按 sha256 分片存放且**可能被多条记录共用**,所以只能在删完行之后 逐个确认「已无人引用」再删文件。 校验那一步断言的是「**没有新增**外键孤儿」,不是「零孤儿」: 实测库里本来就有存量孤儿(agent_keys 指向已不存在的 user、3 封回复的 parent_mail_id 悬空),断言零孤儿会永远红 —— 下一个人就会当它坏了而忽略它。 本次实删:会话 76 / 邮件 290 / 已读标记 60 / 附件 80 / 转发 81 / 权限请求 63, 附件文件 22 个;foreign_key_check 与删除前逐条一致(无新增孤儿)。 另跑 prune-deploy-artifacts.sh --apply 释放 980MB 部署残留(/opt/agentmail 1.3G → 306M)。 --- deploy/prune-test-sessions.sh | 151 ++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 deploy/prune-test-sessions.sh diff --git a/deploy/prune-test-sessions.sh b/deploy/prune-test-sessions.sh new file mode 100644 index 0000000..f38fb32 --- /dev/null +++ b/deploy/prune-test-sessions.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# prune-test-sessions.sh —— 清理**测试会话**(默认干跑,--apply 才落地) +# +# # 为什么需要它 +# +# 用户(2026-09-14):「测试会话清理一下」。 +# +# 手工清不可复现,而且这里有两个容易搞错的地方: +# +# ① **只看「inbox 里看得见的那几条」会漏掉大头**。`ListInbox` 会整体排除 +# `s.status='archived'`(server/internal/repo/repo.go),所以归档的测试会话 +# 在收件箱里根本不出现 —— 但它们在库里、在 `agent_platform_sessions` 的 +# 配额统计里都还在。按别名/来源判定才是干净的。 +# +# ② **直接 DELETE sessions 会留下孤儿**。`mails.session_id`、`mail_reads.mail_id`、 +# `relayed_mails.mail_id`、`attachments.mail_id`、`permission_requests` 都指向 +# 它们(`attachments` 有 ON DELETE CASCADE,其余没有)。必须**子表先删**。 +# +# 判定用「来源 agent」而不是「名字里有 test」: +# `gui-lab` 就是为此存在的专用测试 agent,它发出的会话一律是测试。 +# 剩下的用别名前缀白名单兜(演练/端到端/冒烟/部署检查)。 +# +# # 用法 +# +# bash deploy/prune-test-sessions.sh # 干跑:列出会删什么 +# bash deploy/prune-test-sessions.sh --apply # 备份 + 事务删除 + 校验 +# +# 退出码: 0=成功(干跑也算成功) 1=校验不过(事务已回滚) 2=环境问题 + +set -uo pipefail + +DB="${AGENTMAIL_DB:-/opt/agentmail/data/agentmail.db}" +DATA="${AGENTMAIL_DATA_DIR:-/opt/agentmail/data}" +ATT_DIR="$DATA/attachments" +APPLY=0 +[ "${1:-}" = "--apply" ] && APPLY=1 + +# 测试会话的判定。改这里就能调范围 —— 判定写在脚本里,不写在某个人的脑子里。 +TEST_WHERE="${TEST_WHERE:-( + s.from_agent = 'gui-lab' + OR s.session_alias LIKE 'gui-lab-%' + OR s.session_alias LIKE 'drill-%' + OR s.session_alias LIKE 'e2e3-%' + OR s.session_alias LIKE 'smoke-%' + OR s.session_alias LIKE 'probe-%' + OR s.session_alias LIKE 'deploy-check-%' + OR s.session_alias LIKE 't1-%' + OR s.session_alias LIKE 't2-%' + OR s.session_alias LIKE 't3-%' + OR s.session_alias LIKE '全流程演练-FLOW-%' + OR s.session_alias IN ('scope-会话甲','zcode-正文可读性检验','quick-circuit', + 'jianf-已读语义探针-A','jianf-已读语义复验-B') +)}" + +say() { printf '\n=== %s\n' "$*"; } +ok() { printf ' [ OK ] %s\n' "$*"; } +bad() { printf ' [FAIL] %s\n' "$*"; } + +[ -f "$DB" ] || { bad "找不到数据库 $DB"; exit 2; } +command -v sqlite3 >/dev/null 2>&1 || { bad "缺少 sqlite3"; exit 2; } + +q() { sqlite3 -cmd '.timeout 10000' "$DB" "$@"; } + +say "0. 判定范围" +printf ' 数据库: %s\n' "$DB" +printf ' 条件 : from_agent=gui-lab / 别名前缀 gui-lab- drill- e2e3- smoke- probe- t1- t2- t3- 全流程演练-FLOW-\n' +printf ' 模式 : %s\n' "$([ "$APPLY" = 1 ] && echo '真实删除' || echo '干跑(只报告)')" + +say "1. 将被清理的会话" +q -header -column "select coalesce(s.session_alias,'(无别名)') alias, s.from_agent, s.status, + (select count(*) from mails m where m.session_id=s.session_id) mails + from sessions s where $TEST_WHERE order by s.created_at;" + +NSESS=$(q "select count(*) from sessions s where $TEST_WHERE;") +NMAIL=$(q "select count(*) from mails where session_id in (select session_id from sessions s where $TEST_WHERE);") +NREAD=$(q "select count(*) from mail_reads where mail_id in (select mail_id from mails where session_id in (select session_id from sessions s where $TEST_WHERE));") +NATT=$(q "select count(*) from attachments where mail_id in (select mail_id from mails where session_id in (select session_id from sessions s where $TEST_WHERE));") +NREL=$(q "select count(*) from relayed_mails where mail_id in (select mail_id from mails where session_id in (select session_id from sessions s where $TEST_WHERE));") +NPERM=$(q "select count(*) from permission_requests where session_id in (select session_id from sessions s where $TEST_WHERE);") + +say "2. 连带删除的子表行" +printf ' 会话 %s / 邮件 %s / 已读标记 %s / 附件 %s / 转发记录 %s / 权限请求 %s\n' \ + "$NSESS" "$NMAIL" "$NREAD" "$NATT" "$NREL" "$NPERM" + +if [ "$APPLY" != 1 ]; then + say "干跑结束" + echo " 确认无误会删的东西后加 --apply 重跑。" + exit 0 +fi + +TS="$(date +%Y%m%d-%H%M%S)" +BAK="/tmp/agentmail-pre-prune-$TS.db" + +say "3. 备份(sqlite3 .backup,不能用 cp —— WAL 下 cp 是不一致快照)" +q ".backup '$BAK'" || { bad "备份失败,终止"; exit 1; } +integ="$(sqlite3 "$BAK" 'PRAGMA integrity_check;' 2>&1 | head -1)" +[ "$integ" = "ok" ] || { bad "备份 integrity_check=$integ"; exit 1; } +ok "备份 $BAK($(du -h "$BAK" | cut -f1)),integrity_check=ok" + +# 删除**前**就先记下外键孤儿。库里本来就有存量孤儿(实测:agent_keys 指向已不存在的 +# user、3 封回复的 parent_mail_id 悬空),它们是历史遗留,不是这次清理造成的。 +# 断言「零孤儿」会永远红 → 下一个人会以为脚本坏了而忽略它。要断言的是**没有新增**。 +FK_BEFORE="$(q 'PRAGMA foreign_key_check;' | sort)" +[ -n "$FK_BEFORE" ] && printf ' [ 注 ] 删除前已有 %s 条存量外键孤儿(历史遗留,不计入本次)\n' "$(printf '%s\n' "$FK_BEFORE" | wc -l)" + +# 被删附件的 sha 先记下来:文件按 sha256 分片存放,同一份内容可能被多条记录共用, +# 所以不能直接删文件,得等删完行再看还有没有别人引用。 +SHAS="$(q "select distinct sha256 from attachments where mail_id in (select mail_id from mails where session_id in (select session_id from sessions s where $TEST_WHERE));")" + +say "4. 事务删除(子表先删;任何一步不过就整体回滚)" +q <