All checks were successful
Deploy play.wenpai.net / deploy (push) Successful in 5s
- 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/
192 lines
6.8 KiB
Bash
Executable file
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>&-
|