apple / scripts /openclaw-auto-approve.sh
GGSheng's picture
feat: deploy to hf space | model=claude-72604b
62f6d9a verified
Raw
History Blame Contribute Delete
10.2 kB
#!/bin/bash
#
# openclaw-auto-approve.sh
# 自动扫描并批准 openclaw 配对请求(多通道 + 多账户全批准)
#
# 手动批准请求
# ============
# 当自动批准不工作(CLI 路径卡住 / 通道断连 / daemon 一直没起)时,
# 可以走下面两条兜底路径。
#
# 1) 停 daemon,单条用 openclaw CLI 批准(最常见)
# supervisorctl stop openclaw-auto-approve
# openclaw pairing list --channel <ch> --account <acc> # 看请求码
# openclaw pairing approve <ch> <code> --account <acc> # 批准一条
# supervisorctl start openclaw-auto-approve
#
# 2) 调试用 DRY_RUN 看脚本打算做什么而不真批
# DRY_RUN=1 启动一次,看 /var/log/openclaw/openclaw-auto-approve.log
# 确认后再用 #1 真批
#
# 设计要点:
# - 通过 `openclaw channels list --json` 自动发现已安装且有账户的通道
# - 每个通道、每个账户都扫描一次 `pairing list --json`,逐条批准
# - 状态文件记录已批准过的 (channel, account, code),重启不重复批
# - PID 文件防止多实例同时跑
# - 优雅退出(SIGTERM/SIGINT/SIGHUP)
# - DRY_RUN=1 模式只打印不真批
# - CHANNEL 环境变量可显式锁定到单通道(默认自动发现)
#
# 环境变量:
# CHANNEL 显式锁定到单通道(默认空 = 自动发现所有已装通道)
# INTERVAL 扫描间隔秒数,默认 10
# LOG_DIR 日志目录,默认 /var/log/openclaw
# STATE_DIR 状态目录,默认 /var/lib/openclaw
# DRY_RUN 1 = 只打印要批准什么不真批准
# PREFIX openclaw 之外的命令前缀(一般空着)
# OPENCLAW_BIN openclaw 可执行路径,默认 /usr/bin/openclaw
set -uo pipefail
CHANNEL="${CHANNEL:-}"
INTERVAL="${INTERVAL:-10}"
LOG_DIR="${LOG_DIR:-/var/log/openclaw}"
STATE_DIR="${STATE_DIR:-/var/lib/openclaw}"
LOG_FILE="$LOG_DIR/openclaw-auto-approve.log"
STATE_FILE="$STATE_DIR/auto-approve.approved"
PID_FILE="$STATE_DIR/auto-approve.pid"
DRY_RUN="${DRY_RUN:-0}"
PREFIX="${PREFIX:-}"
OPENCLAW_BIN="${OPENCLAW_BIN:-/usr/bin/openclaw}"
mkdir -p "$LOG_DIR" "$STATE_DIR"
# ---- 工具函数 ----
log() {
local level="$1"; shift
printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$*" | tee -a "$LOG_FILE" >&2
}
run_oc() {
if [ -n "$PREFIX" ]; then
eval "$PREFIX $OPENCLAW_BIN $*"
else
"$OPENCLAW_BIN" "$@"
fi
}
# ---- JSON 解析 ----
# 用 jq 最稳;没有就退化 python3
parse_json_codes() {
local payload="$1"
if command -v jq >/dev/null 2>&1; then
printf '%s' "$payload" | jq -r '.requests[]?.code // empty' 2>/dev/null
elif command -v python3 >/dev/null 2>&1; then
printf '%s' "$payload" | python3 -c '
import json, sys
try:
data = json.load(sys.stdin)
for r in data.get("requests", []):
if r.get("code"):
print(r["code"])
except Exception:
pass
' 2>/dev/null
else
log "ERROR" "需要 jq 或 python3 来解析 JSON,二者都不可用"
return 1
fi
}
# 从 channels list --json 里抓 (channel, account) 对
# 输出格式:每行 "<channel> <account>"
parse_json_channel_accounts() {
local payload="$1"
if command -v jq >/dev/null 2>&1; then
printf '%s' "$payload" | jq -r '
.chat // {} | to_entries[]? as $ch
| select($ch.value.installed == true)
| select(($ch.value.accounts // []) | length > 0)
| $ch.value.accounts[] as $acc
| [$ch.key, $acc] | join(" ")
' 2>/dev/null
elif command -v python3 >/dev/null 2>&1; then
printf '%s' "$payload" | python3 -c '
import json, sys
try:
data = json.load(sys.stdin)
chat = data.get("chat", {})
for ch_id, info in chat.items():
if not info.get("installed"):
continue
accounts = info.get("accounts") or []
for acc in accounts:
print(f"{ch_id} {acc}")
except Exception:
pass
' 2>/dev/null
else
return 1
fi
}
# ---- 单实例锁 ----
acquire_lock() {
if [ -f "$PID_FILE" ]; then
local old_pid
old_pid=$(cat "$PID_FILE" 2>/dev/null || echo "")
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
log "ERROR" "已有实例在运行 (PID: $old_pid),退出"
exit 1
fi
log "WARN" "残留 PID 文件 (PID: $old_pid 不存在),清理"
rm -f "$PID_FILE"
fi
echo $$ > "$PID_FILE"
}
release_lock() {
rm -f "$PID_FILE"
}
shutdown() {
log "INFO" "收到终止信号,关闭..."
release_lock
exit 0
}
trap shutdown SIGTERM SIGINT SIGHUP
# ---- 状态去重 ----
# 状态文件每行格式:<channel>|<account>|<code>
# 兼容旧格式:裸 code 行(无 |)视为 feishu|default|<code>
state_key() {
local ch="$1" acc="$2" code="$3"
printf '%s|%s|%s' "$ch" "$acc" "$code"
}
is_approved() {
local key="$1"
# 优先匹配新格式
if grep -Fxq "$key" "$STATE_FILE" 2>/dev/null; then
return 0
fi
# 兼容旧格式:若 key 是 feishu|default|<code>,匹配裸 code
if [[ "$key" == "feishu|default|"* ]]; then
local bare_code="${key#feishu|default|}"
if grep -Fxq "$bare_code" "$STATE_FILE" 2>/dev/null; then
return 0
fi
fi
return 1
}
mark_approved() {
local key="$1"
printf '%s\n' "$key" >> "$STATE_FILE"
}
# ---- 主循环 ----
main() {
acquire_lock
# 启动时一次状态文件迁移:把旧裸 code 升级为新格式
touch "$STATE_FILE"
if [ -s "$STATE_FILE" ] && grep -q -v '|' "$STATE_FILE" 2>/dev/null; then
log "INFO" "检测到旧格式状态文件,迁移为 channel|account|code"
local migrated
migrated=$(awk -F'|' '
NF==1 && $1!="" {print "feishu|default|"$1; next}
{print}
' "$STATE_FILE")
printf '%s\n' "$migrated" > "$STATE_FILE"
fi
if [ -n "$CHANNEL" ]; then
log "INFO" "启动自动批准服务 (mode=锁定通道 channel=$CHANNEL interval=${INTERVAL}s dry_run=$DRY_RUN pid=$$)"
else
log "INFO" "启动自动批准服务 (mode=自动发现所有通道 interval=${INTERVAL}s dry_run=$DRY_RUN pid=$$)"
fi
while true; do
local tick_channels=()
local tick_accounts=()
# 决定本轮要扫描的 (channel, account) 列表
if [ -n "$CHANNEL" ]; then
# 显式锁定:单通道单账户
tick_channels=("$CHANNEL")
tick_accounts=("default")
else
# 自动发现:拉 channels list --json
local channels_json
if ! channels_json=$(run_oc channels list --json 2>&1); then
log "ERROR" "channels list 失败: $channels_json"
sleep "$INTERVAL"
continue
fi
local pairs
pairs=$(parse_json_channel_accounts "$channels_json")
if [ -z "$pairs" ]; then
log "INFO" "当前没有已安装且有账户的通道"
sleep "$INTERVAL"
continue
fi
while IFS=' ' read -r ch acc; do
[ -z "$ch" ] && continue
tick_channels+=("$ch")
tick_accounts+=("$acc")
done <<< "$pairs"
fi
if [ ${#tick_channels[@]} -eq 0 ]; then
log "INFO" "无可扫描通道"
sleep "$INTERVAL"
continue
fi
local total_found=0
local total_approved=0
local total_skipped=0
local total_failed=0
# 遍历 (channel, account)
for idx in "${!tick_channels[@]}"; do
local ch="${tick_channels[$idx]}"
local acc="${tick_accounts[$idx]}"
# 拉该通道该账户的待批列表
local list_json
if ! list_json=$(run_oc pairing list --channel "$ch" --account "$acc" --json 2>&1); then
# 通道不支持 pairing / 账户不存在 / 通道未启用等情况
log "DEBUG" "跳过 $ch/$acc: $list_json"
continue
fi
local codes=()
while IFS= read -r c; do
[ -n "$c" ] && codes+=("$c")
done < <(parse_json_codes "$list_json")
if [ ${#codes[@]} -eq 0 ]; then
log "DEBUG" "$ch/$acc 无待批准"
continue
fi
log "INFO" "[$ch/$acc] 发现 ${#codes[@]} 个待批准请求"
total_found=$((total_found + ${#codes[@]}))
for code in "${codes[@]}"; do
local key
key=$(state_key "$ch" "$acc" "$code")
if is_approved "$key"; then
log "INFO" "[$ch/$acc] 跳过已批准过的 $code"
total_skipped=$((total_skipped + 1))
continue
fi
log "INFO" "[$ch/$acc] 准备批准: $code"
if [ "$DRY_RUN" = "1" ]; then
log "INFO" "[$ch/$acc] [DRY-RUN] 不会真调用 approve"
continue
fi
local result
result=$(run_oc pairing approve "$ch" "$code" --account "$acc" --notify 2>&1)
local approve_rc=$?
echo "$result" >> "$LOG_FILE"
if [ $approve_rc -eq 0 ] || echo "$result" | grep -qi "approved"; then
mark_approved "$key"
total_approved=$((total_approved + 1))
log "INFO" "✅ [$ch/$acc] 已批准并通知: $code"
else
total_failed=$((total_failed + 1))
log "ERROR" "❌ [$ch/$acc] 批准失败 (rc=$approve_rc): $code"
fi
done
done
if [ $total_found -eq 0 ]; then
log "INFO" "本轮扫描 ${#tick_channels[@]} 个通道,无待批准请求"
else
log "INFO" "本轮汇总: 通道=${#tick_channels[@]} 发现=$total_found 新批准=$total_approved 跳过=$total_skipped 失败=$total_failed"
fi
sleep "$INTERVAL"
done
}
main