Files
MailUI4Agents/deploy/prune-test-sessions.sh
JianFeeeee f9b849dcfc 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)。
2026-09-14 15:51:40 +08:00

152 lines
7.8 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"