play.wenpai.net/scripts/inbox-reactor.sh
elementary-qa bddd9840cb
All checks were successful
Deploy play.wenpai.net / deploy (push) Successful in 5s
feat: add missing plugin sha256 checksums and operational tooling
- add sha256 for wpavatar, wp-china-yes, wpdate, wpfonts, wpmind
- restore accidentally deleted wpfonts-1.2.0.zip from git HEAD
- add NATS messaging wrappers (vm-dm.sh, vm-say.sh, lib/nats-msg.sh)
- add inbox automation hooks (inbox-reactor.sh, daemon, codex hook)
- symlink generate-html-report.js from qa/scripts/
2026-04-11 04:22:53 +08:00

192 lines
6.8 KiB
Bash
Executable file

#!/bin/bash
set -euo pipefail
# inbox-reactor.sh — 实时消息分类处理
# 用法: inbox-reactor.sh <inbox-json-file>
# 分类: auto-archive / action / reply / default→action
MSG_FILE="${1:-}"
[[ -z "$MSG_FILE" || ! -f "$MSG_FILE" ]] && exit 0
CACHE_DIR="${HOME}/.cache/vm-watcher"
LOG_FILE="${CACHE_DIR}/reactor.log"
LOCK_FILE="${CACHE_DIR}/reactor.lock"
TOKEN_LOG="${CACHE_DIR}/reactor-tokens.log"
DEFERRED_LOG="${CACHE_DIR}/reactor-deferred.log"
DEPTH_FILE="${CACHE_DIR}/conversation-depth.json"
MAX_AI_PER_HOUR=3
MAX_CONVERSATION_DEPTH=3
CODEX_BIN="$(command -v codex 2>/dev/null || echo "${HOME}/.local/bin/codex")"
VM_NAME="$(cat ~/.vm-name 2>/dev/null || cat /etc/shared-context-vmname 2>/dev/null || hostname -s)"
VM_DM="$(command -v vm-dm 2>/dev/null || echo "${HOME}/scripts/vm-dm.sh")"
VM_INBOX="$(command -v vm-inbox 2>/dev/null || echo "")"
mkdir -p "$CACHE_DIR"
[[ -f "$DEPTH_FILE" ]] || echo '{}' > "$DEPTH_FILE"
[[ -f "$TOKEN_LOG" ]] || touch "$TOKEN_LOG"
log() { echo "[$(date -Iseconds)] $*" >> "$LOG_FILE"; }
# 日志轮转:超过 5MB 保留最后 1000 行
if [[ -f "$LOG_FILE" ]] && (( $(stat -c%s "$LOG_FILE" 2>/dev/null || stat -f%z "$LOG_FILE" 2>/dev/null || echo 0) > 5242880 )); then
tail -1000 "$LOG_FILE" > "${LOG_FILE}.tmp" && mv "${LOG_FILE}.tmp" "$LOG_FILE"
fi
# flock 单实例
exec 201>"$LOCK_FILE"
flock -w 30 201 || { log "SKIP lock timeout: $(basename "$MSG_FILE")"; exit 0; }
# 读取消息字段
msg_id=$(jq -r '.id // ""' "$MSG_FILE" 2>/dev/null || echo "")
msg_from=$(jq -r '.from // "unknown"' "$MSG_FILE" 2>/dev/null || echo "unknown")
msg_type=$(jq -r '.type // ""' "$MSG_FILE" 2>/dev/null || echo "")
msg_priority=$(jq -r '.priority // "normal"' "$MSG_FILE" 2>/dev/null || echo "normal")
msg_subject=$(jq -r '.subject // ""' "$MSG_FILE" 2>/dev/null || echo "")
msg_content=$(jq -r '.content // ""' "$MSG_FILE" 2>/dev/null || echo "")
msg_basename=$(basename "$MSG_FILE")
# ── 分类 ──
classify() {
# auto-archive: 通知/状态/批量报告
case "$msg_type" in
notification|info|status-reply) echo "auto-archive"; return ;;
esac
case "$msg_subject" in
*dispatch-done*|*batch-update*|*link-scan*|*daily-check*|*nuclei-update*|*app-monitor*|*session-sync*) echo "auto-archive"; return ;;
esac
# content 里的通知模式也自动归档
if echo "$msg_content" | grep -qE '^\[link-scan\]|^\[batch-update\]|^\[自动更新\]|巡检正常|巡检完成'; then
echo "auto-archive"; return
fi
# action: 紧急或含动作词
[[ "$msg_priority" == "urgent" ]] && { echo "action"; return; }
[[ "$msg_type" == "dispatch" ]] && { echo "action"; return; }
if echo "$msg_content" | grep -qE '请回复|请处理|请检查|帮我|收到请回|请确认|帮忙'; then
echo "action"; return
fi
# reply: 对话续接
if echo "$msg_subject" | grep -qiE '^re:|reply|回复'; then
echo "reply"; return
fi
# default → action
echo "action"
}
category=$(classify)
log "MSG=$msg_basename FROM=$msg_from CAT=$category SUBJ=${msg_subject:-(none)}"
# ── 归档函数 ──
archive_msg() {
if [[ -n "$VM_INBOX" ]]; then
"$VM_INBOX" --ack-by-id "$msg_id" 2>/dev/null || "$VM_INBOX" --ack "$msg_basename" 2>/dev/null || true
else
mkdir -p "${HOME}/inbox/archive"
mv "$MSG_FILE" "${HOME}/inbox/archive/" 2>/dev/null || true
fi
}
# ── token 限流检查 ──
check_rate_limit() {
local cutoff now count
now=$(date +%s)
cutoff=$((now - 3600))
# 清理 1 小时前的记录
if [[ -s "$TOKEN_LOG" ]]; then
awk -v c="$cutoff" '$1 >= c' "$TOKEN_LOG" > "${TOKEN_LOG}.tmp" && mv "${TOKEN_LOG}.tmp" "$TOKEN_LOG"
fi
count=$(wc -l < "$TOKEN_LOG" 2>/dev/null || echo 0)
(( count < MAX_AI_PER_HOUR ))
}
# ── 记录 AI 调用 ──
record_ai_call() {
echo "$(date +%s) $msg_id" >> "$TOKEN_LOG"
}
# ── 对话深度检查 ──
check_conversation_depth() {
local from="$1"
local now depth last_time
now=$(date +%s)
# 读取当前深度
depth=$(jq -r --arg f "$from" '.[$f].count // 0' "$DEPTH_FILE" 2>/dev/null || echo 0)
last_time=$(jq -r --arg f "$from" '.[$f].last_epoch // 0' "$DEPTH_FILE" 2>/dev/null || echo 0)
# 超过 1 小时没新消息,重置
if (( now - last_time > 3600 )); then
depth=0
fi
echo "$depth"
}
update_conversation_depth() {
local from="$1"
local now
now=$(date +%s)
local current
current=$(check_conversation_depth "$from")
jq --arg f "$from" --argjson c "$((current + 1))" --argjson t "$now" --arg mid "$msg_id" \
'.[$f] = {count: $c, last_msg_id: $mid, last_epoch: $t}' \
"$DEPTH_FILE" > "${DEPTH_FILE}.tmp" && mv "${DEPTH_FILE}.tmp" "$DEPTH_FILE"
}
# ── 启动 AI 处理 ──
run_ai_action() {
local prompt="$1"
if ! check_rate_limit; then
log "RATE_LIMIT: deferred $msg_basename ($(wc -l < "$TOKEN_LOG")/$MAX_AI_PER_HOUR per hour)"
echo "$MSG_FILE" >> "$DEFERRED_LOG"
return 0
fi
record_ai_call
log "AI_START: $msg_basename"
if [[ -x "$CODEX_BIN" ]]; then
timeout 180 "$CODEX_BIN" exec --skip-git-repo-check "$prompt" >> "$LOG_FILE" 2>&1 || true
fi
log "AI_DONE: $msg_basename"
archive_msg
}
# ── 处理 ──
case "$category" in
auto-archive)
archive_msg
log "ARCHIVED: $msg_basename"
;;
action)
prompt="你是 ${VM_NAME},收到来自 ${msg_from} 的消息。
主题: ${msg_subject:-(无)}
内容: ${msg_content}
请处理这条消息。如果需要回复,用 ~/scripts/vm-dm.sh ${msg_from} \"<回复内容>\" 回复。
处理完后用 vm-inbox --ack-by-id ${msg_id} 归档(如果还没归档的话)。
不要执行危险操作(删除文件、修改系统配置、操作密钥)。
简洁处理,不要过度展开。"
run_ai_action "$prompt"
;;
reply)
depth=$(check_conversation_depth "$msg_from")
if (( depth >= MAX_CONVERSATION_DEPTH )); then
log "DEPTH_LIMIT: $msg_from depth=$depth, auto-archive"
"$VM_DM" "$msg_from" "对话已达 ${MAX_CONVERSATION_DEPTH} 轮上限,后续请人工处理。" 2>/dev/null || true
archive_msg
else
update_conversation_depth "$msg_from"
prompt="你是 ${VM_NAME},收到来自 ${msg_from} 的回复消息(对话第 $((depth+1)) 轮)。
主题: ${msg_subject:-(无)}
内容: ${msg_content}
这是一个进行中的对话。请判断:
1. 如果对方只是确认/感谢/无需进一步行动 → 不回复,直接归档
2. 如果需要继续讨论或有新的行动项 → 用 ~/scripts/vm-dm.sh ${msg_from} \"<回复>\" 回复
处理完后用 vm-inbox --ack-by-id ${msg_id} 归档。"
run_ai_action "$prompt"
fi
;;
esac
exec 201>&-