104 lines
2.8 KiB
Bash
Executable File
104 lines
2.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# tg-task — wrap any long-running command so Telegram gets:
|
|
# 1. an immediate "starting" heartbeat before the command runs
|
|
# 2. an interim "still running (Ns elapsed)" heartbeat every HEARTBEAT_SECS
|
|
# 3. a final "done in Ns" + the command's stdout (truncated)
|
|
#
|
|
# Usage:
|
|
# tg-task "label" -- <command...>
|
|
# tg-task --target 3 "rendering avatar" -- python3 /tmp/avatar.py
|
|
# tg-task "apt install python3" -- apt-get install -y python3
|
|
#
|
|
# Env:
|
|
# HEARTBEAT_SECS — interim heartbeat interval (default 8s)
|
|
# TG_STREAM — path to tg-stream (default /host/root/openclaw/tg-stream)
|
|
# TG_CHAT — chat to notify (default tg-stream's default — StarCros)
|
|
#
|
|
# Why this exists: the rule "send a heartbeat every ~5 seconds during long
|
|
# work" was previously a behavioral memory that I had to remember every
|
|
# session. That burns tokens and I can forget. Wrapping it as a script makes
|
|
# the behavior deterministic and free.
|
|
|
|
set -u
|
|
TG_STREAM="${TG_STREAM:-/host/root/openclaw/tg-stream}"
|
|
HEARTBEAT_SECS="${HEARTBEAT_SECS:-8}"
|
|
TARGET=2
|
|
|
|
# Parse options before --
|
|
LABEL=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--target) TARGET="$2"; shift 2;;
|
|
--) shift; break;;
|
|
*)
|
|
if [[ -z "$LABEL" ]]; then LABEL="$1"; shift
|
|
else break
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$LABEL" ]]; then
|
|
echo "usage: tg-task [--target SECS] \"label\" -- <command...>" >&2
|
|
exit 2
|
|
fi
|
|
if [[ $# -eq 0 ]]; then
|
|
echo "tg-task: missing command after --" >&2
|
|
exit 2
|
|
fi
|
|
|
|
START_TS=$(date +%s)
|
|
|
|
# Heartbeat 1: starting
|
|
"$TG_STREAM" --target "$TARGET" "🔧 starting · $LABEL" >/dev/null 2>&1 || true
|
|
|
|
# Run command in background, capture output
|
|
TMPOUT=$(mktemp /tmp/tg-task-out.XXXXXX)
|
|
TMPERR=$(mktemp /tmp/tg-task-err.XXXXXX)
|
|
"$@" >"$TMPOUT" 2>"$TMPERR" &
|
|
PID=$!
|
|
|
|
# Heartbeat loop
|
|
LAST_HEARTBEAT=$START_TS
|
|
while kill -0 "$PID" 2>/dev/null; do
|
|
sleep 1
|
|
NOW=$(date +%s)
|
|
if (( NOW - LAST_HEARTBEAT >= HEARTBEAT_SECS )); then
|
|
ELAPSED=$((NOW - START_TS))
|
|
"$TG_STREAM" --no-stream "⏳ still on it · $LABEL · ${ELAPSED}s elapsed" >/dev/null 2>&1 || true
|
|
LAST_HEARTBEAT=$NOW
|
|
fi
|
|
done
|
|
|
|
wait "$PID"
|
|
RC=$?
|
|
END_TS=$(date +%s)
|
|
DURATION=$((END_TS - START_TS))
|
|
|
|
# Final report
|
|
OUT_SNIPPET=$(tail -c 1500 "$TMPOUT")
|
|
ERR_SNIPPET=$(tail -c 500 "$TMPERR")
|
|
STATUS=$([[ $RC -eq 0 ]] && echo "✅ done" || echo "❌ failed (rc=$RC)")
|
|
|
|
REPORT="$STATUS · $LABEL · ${DURATION}s"
|
|
if [[ -n "$OUT_SNIPPET" ]]; then
|
|
REPORT="$REPORT
|
|
|
|
stdout:
|
|
$OUT_SNIPPET"
|
|
fi
|
|
if [[ -n "$ERR_SNIPPET" && $RC -ne 0 ]]; then
|
|
REPORT="$REPORT
|
|
|
|
stderr:
|
|
$ERR_SNIPPET"
|
|
fi
|
|
|
|
"$TG_STREAM" --target "$TARGET" "$REPORT" >/dev/null 2>&1 || true
|
|
|
|
# Also dump the full output to stdout/stderr so the caller can pipe/inspect
|
|
cat "$TMPOUT"
|
|
[[ -s "$TMPERR" ]] && cat "$TMPERR" >&2
|
|
rm -f "$TMPOUT" "$TMPERR"
|
|
exit $RC
|