chore(deploy): 测试会话清理脚本(可复现、默认干跑、带备份与校验)

用户(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)。
This commit is contained in:
2026-09-14 15:51:40 +08:00
parent 7028c244fd
commit f9b849dcfc

View File

@ -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 <<SQL
PRAGMA foreign_keys=ON;
BEGIN IMMEDIATE;
CREATE TEMP TABLE victims AS SELECT session_id FROM sessions s WHERE $TEST_WHERE;
DELETE FROM mail_reads WHERE mail_id IN (SELECT mail_id FROM mails WHERE session_id IN (SELECT session_id FROM victims));
DELETE FROM permission_requests WHERE session_id IN (SELECT session_id FROM victims);
DELETE FROM relayed_mails WHERE mail_id IN (SELECT mail_id FROM mails WHERE session_id IN (SELECT session_id FROM victims));
DELETE FROM attachments WHERE mail_id IN (SELECT mail_id FROM mails WHERE session_id IN (SELECT session_id FROM victims));
DELETE FROM mails WHERE session_id IN (SELECT session_id FROM victims);
DELETE FROM sessions WHERE session_id IN (SELECT session_id FROM victims);
COMMIT;
SQL
rc=$?
[ "$rc" = 0 ] || { bad "删除事务失败(已回滚),退出码 $rc"; exit 1; }
ok "已删除"
say "5. 清理不再被引用的附件文件(按 sha256 判,共用的不删)"
removed=0
while read -r s; do
[ -n "$s" ] || continue
n="$(q "select count(*) from attachments where sha256='$s';")"
[ "$n" = "0" ] || continue
f="$ATT_DIR/${s:0:2}/${s:2:2}/$s"
[ -f "$f" ] && { rm -f "$f"; removed=$((removed+1)); }
done <<< "$SHAS"
ok "删除附件文件 $removed"
say "6. 校验"
left="$(q "select count(*) from sessions s where $TEST_WHERE;")"
[ "$left" = "0" ] && ok "测试会话已清空" || bad "还剩 $left 条测试会话"
# 比的是「有没有新增孤儿」,不是「有没有孤儿」(见第 3 步的说明)。
FK_AFTER="$(q 'PRAGMA foreign_key_check;' | sort)"
NEW_FK="$(comm -13 <(printf '%s\n' "$FK_BEFORE") <(printf '%s\n' "$FK_AFTER"))"
[ -z "$NEW_FK" ] && ok "外键无新增孤儿(存量 $(printf '%s\n' "$FK_AFTER" | grep -c . ) 条,与删除前一致)" \
|| bad "本次清理引入了新的外键孤儿:\n$NEW_FK"
echo " 剩余会话 $(q 'select count(*) from sessions;') / 邮件 $(q 'select count(*) from mails;')(清理前 会话 $(( $(q 'select count(*) from sessions;') + NSESS )) / 邮件 $(( $(q 'select count(*) from mails;') + NMAIL ))"
echo
echo " 回滚(先停服务):"
echo " systemctl stop agentmail-gateway"
echo " sqlite3 $BAK \".backup '$DB'\""
echo " systemctl start agentmail-gateway"