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:
151
deploy/prune-test-sessions.sh
Normal file
151
deploy/prune-test-sessions.sh
Normal 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"
|
||||
Reference in New Issue
Block a user