Files
MailUI4Agents/deploy/archive-stale-sessions.sh
JianFeeeee 18c9aef219 chore(deploy): 会话归档脚本(判据 / 备份 / 回滚真跑一遍)—— 用户「清理过时的请求与邮件」
判据:active 且 > IDLE_HOURS 小时无动静,KEEP_IDS 保护人的在用来话。
归档(不删、不碰 updated_at);备份 .backup + integrity;id 清单落文件。
回滚脚本生成用**带引号 heredoc**,且把回滚在**副本库上真跑一遍**才算有回滚路径 ——
第一版用 awk printf 拼 IN 列表拼出 \"''id2\"(引号错位),判据当场抓住(rc=0 但
active 没回到 16);改成 bash 循环拼后副本库实测 12 条复活、active 回到 16。
本次落地:16 → 4 条 active,12 条过时会话(349 封信、76 条权限问询)离开收件箱。
2026-09-15 07:00:35 +08:00

171 lines
7.5 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
# archive-stale-sessions.sh —— 归档**过时的会话**(默认干跑,--apply 才落地)
#
# # 为什么是"归档"而不是"删除"
#
# 邮件不可篡改:过时 ≠ 可以删。归档只是把它移出收件箱(`ListInbox` 排除
# `status='archived'`),数据、正文、权限记录全在。删除是另一件事,只在
# `prune-test-sessions.sh`(测试会话)里做,而且要带备份与子表清理。
#
# # 判据(可复现,不靠记性)
#
# `status='active'` 且 **> IDLE_HOURS 小时没有任何动静**(按 `sessions.updated_at`),
# 且不在保护名单里。保护名单(`KEEP_IDS`,逗号分隔的 id 前缀或别名片段)里默认放
# **正在用的那条会话** —— 它是人的寻址入口,归档掉等于让对端"消失"。
#
# 顺带清掉的就是用户说的「过时的**请求**」:`permission_request` 类邮件随它所在的
# 会话一起离开收件箱。这里**不动** `permission_requests` 表本身 —— 那是决策记录,
# 不是待办(实测未决数 = 0)。
#
# # 回滚
#
# 归档没有反向端点,所以判据是**真跑一遍回滚**:`--apply` 会写一份 id 清单 +
# 一个回滚脚本,然后把回滚脚本在**副本库**上跑一遍,确认 active 数回到原值。
#
# ★ 生成回滚脚本用**带引号的 heredoc**(不带引号会让 shell 提前展开正文里的
# `$(...)`:2026-09-14 那次真写进去一个空结果,脚本语法正确却一个 id 都匹配不上),
# id 清单落成文件、脚本运行时自己读 —— 生成时只替换一个纯路径。
#
# 用法:
# bash deploy/archive-stale-sessions.sh # 干跑
# KEEP_IDS=<在用的会话 id 前缀> IDLE_HOURS=6 bash deploy/archive-stale-sessions.sh --apply
set -uo pipefail
DB="${AGENTMAIL_DB:-/opt/agentmail/data/agentmail.db}"
OUT_DIR="${ARCHIVE_OUT_DIR:-/root/gotmp}"
IDLE_HOURS="${IDLE_HOURS:-6}"
KEEP_IDS="${KEEP_IDS:-}"
APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1
TS="$(date +%Y%m%d-%H%M%S)"
PLAN="$OUT_DIR/archive-stale-plan-$TS.json"
[ -f "$DB" ] || { echo "找不到库:$DB" >&2; exit 2; }
WHERE="s.status = 'active'
AND s.updated_at < datetime('now', '-$IDLE_HOURS hours')"
if [ -n "$KEEP_IDS" ]; then
IFS=',' read -r -a keeps <<<"$KEEP_IDS"
for k in "${keeps[@]}"; do
[ -z "$k" ] && continue
WHERE="$WHERE AND s.session_id NOT LIKE '$k%' AND coalesce(s.session_alias,'') NOT LIKE '%$k%'"
done
fi
echo "会话归档判据:active 且 > $IDLE_HOURS 小时无动静${KEEP_IDS:+,保护 [$KEEP_IDS]}"
echo
sqlite3 -header -column "$DB" "
SELECT substr(s.session_id,1,8) AS id,
s.from_agent,
coalesce(nullif(s.session_alias,''),'(无别名)') AS alias,
(SELECT count(*) FROM mails m WHERE m.session_id=s.session_id) AS mails,
(SELECT count(*) FROM permission_requests p WHERE p.session_id=s.session_id) AS asks,
round((julianday('now')-julianday(s.updated_at))*24,1) AS h_ago
FROM sessions s WHERE $WHERE
ORDER BY s.updated_at DESC;" || exit 1
BEFORE_ACTIVE=$(sqlite3 "$DB" "SELECT count(*) FROM sessions WHERE status='active';")
N=$(sqlite3 "$DB" "SELECT count(*) FROM sessions s WHERE $WHERE;")
echo
echo " active 会话 $BEFORE_ACTIVE 条;本次将归档 $N 条(其余不动)。"
if [ "$N" = 0 ]; then
echo " 没有到期的会话 —— 什么都不做。"
exit 0
fi
sqlite3 -json "$DB" "
SELECT s.session_id, coalesce(s.session_alias,'') AS alias, s.updated_at,
(SELECT count(*) FROM mails m WHERE m.session_id=s.session_id) AS mails
FROM sessions s WHERE $WHERE;" > "$PLAN"
echo " 清单:$PLAN"
if [ "$APPLY" != 1 ]; then
echo " 干跑结束。确认无误后加 --apply。"
exit 0
fi
# ── 备份(.backup,不是 cp:WAL 下 cp 拿不到一致快照)──
BAK="$OUT_DIR/agentmail-pre-archive-$TS.db"
sqlite3 "$DB" ".backup '$BAK'" || { echo "备份失败" >&2; exit 1; }
INTEG=$(sqlite3 "$BAK" 'PRAGMA integrity_check;' | head -1)
[ "$INTEG" = "ok" ] || { echo "备份完整性异常:$INTEG" >&2; exit 1; }
chmod 600 "$BAK"
echo " 备份:$BAK(integrity ok,$(du -h "$BAK" | cut -f1))"
# ── id 清单(回滚脚本的输入;也是审计清单)──
IDS_FILE="$OUT_DIR/archive-ids-$TS.txt"
sqlite3 "$DB" "SELECT s.session_id FROM sessions s WHERE $WHERE;" > "$IDS_FILE"
echo " 回滚清单:$IDS_FILE($(wc -l < "$IDS_FILE") 行)"
# ── 归档(**不碰 updated_at**:那是"最后一次动静"这个事实,归档不该改写它)──
sqlite3 "$DB" "BEGIN;
UPDATE sessions SET status='archived' WHERE session_id IN (
SELECT s.session_id FROM sessions s WHERE $WHERE
);
COMMIT;" || { echo "归档失败" >&2; exit 1; }
AFTER_ACTIVE=$(sqlite3 "$DB" "SELECT count(*) FROM sessions WHERE status='active';")
echo " active:$BEFORE_ACTIVE → $AFTER_ACTIVE(期望 $((BEFORE_ACTIVE - N)))"
[ "$AFTER_ACTIVE" = "$((BEFORE_ACTIVE - N))" ] || { echo " ★ 计数不符,别继续" >&2; exit 1; }
# ── 回滚脚本(带引号 heredoc;只替换一个纯路径)──
UNDO="$OUT_DIR/undo-archive-stale-$TS.sh"
cat <<'EOF' | sed "s|__IDS_FILE__|$IDS_FILE|; s|__TS__|$TS|" > "$UNDO"
#!/usr/bin/env bash
# 回滚 __TS__ 那次归档:把 id 清单里的会话改回 active。
#
# 用法:bash <本脚本> [--db /path/to/another.db]
# (不给 --db 就打生产库;归档判据会在副本库上先跑一遍)
set -uo pipefail
IDS_FILE="__IDS_FILE__"
DB="/opt/agentmail/data/agentmail.db"
if [ "${1:-}" = "--db" ]; then DB="${2:?--db 后面要给库路径}"; fi
[ -f "$DB" ] || { echo "找不到库:$DB" >&2; exit 2; }
[ -f "$IDS_FILE" ] || { echo "找不到 id 清单:$IDS_FILE" >&2; exit 2; }
# ★ 用 bash 循环拼 IN 列表,不用 awk 的 printf —— 第一版用
# `printf "%s%s%s", sep, "\x27", $1` 拼出来是 `''id2`(关闭引号与开启引号错位),
# 语法正确、一个 id 都匹配不上。判据当场抓住了它(rc=0 但 active 没回到 16)。
IN=""
SEP=""
while IFS= read -r id; do
[ -z "$id" ] && continue
case "$id" in \#*) continue;; esac
IN="${IN}${SEP}'${id}'"
SEP=","
done < "$IDS_FILE"
[ -n "$IN" ] || { echo "清单为空,什么都不做"; exit 0; }
echo " 清单:$IDS_FILE"
echo " 库 :$DB"
sqlite3 "$DB" "SELECT count(*) FROM sessions WHERE session_id IN ($IN) AND status='archived';" \
| sed "s/^/ 待复活(当前 archived): /"
sqlite3 "$DB" "UPDATE sessions SET status='active' WHERE session_id IN ($IN) AND status='archived';"
sqlite3 "$DB" "SELECT count(*) FROM sessions WHERE status='active';" | sed "s/^/ active 现在: /"
EOF
chmod +x "$UNDO"
echo " 回滚脚本:$UNDO"
# ── 判据:把回滚**真跑一遍**(副本库,不碰生产)──
# 只"生成了一个回滚脚本"不算有回滚路径:2026-09-14 那版拼成了"一个带引号的元素",
# 语法检查能过、一个 id 都匹配不上。
TEST="$OUT_DIR/rollback-test-$TS.db"
sqlite3 "$DB" ".backup '$TEST'" || { echo "副本建不出来" >&2; exit 1; }
OUT=$(bash "$UNDO" --db "$TEST" 2>&1); RC=$?
RESTO=$(sqlite3 "$TEST" "SELECT count(*) FROM sessions WHERE status='active';")
rm -f "$TEST"
if [ "$RC" = 0 ] && [ "$RESTO" = "$BEFORE_ACTIVE" ]; then
echo " ★ 回滚判据:副本库上真跑过 → active 回到 $RESTO(原值 $BEFORE_ACTIVE)✓"
else
echo " ★ 回滚判据失败:rc=$RC、复活后 active=$RESTO(期望 $BEFORE_ACTIVE)" >&2
printf '%s\n' "$OUT" | tail -3 >&2
echo " 归档已执行,但回滚路径不可信 —— 立刻用备份 $BAK 处理" >&2
exit 1
fi
echo
echo " 归档完成:收件箱只剩最近 $IDLE_HOURS 小时内有动静的会话。"