Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e630ac08e0 |
@@ -64,23 +64,31 @@ This keeps `autonomy.workspace_only = true` effective on a per-repo boundary.
|
||||
- `tools/ai/zeroclaw_dual_chat.sh`
|
||||
- target switch by alias (`rtc`, `zacus`) or absolute path,
|
||||
- message mode (`-m`) or interactive mode,
|
||||
- loads local auth env file `~/.zeroclaw/env` when present,
|
||||
- provider auto-fallback (`copilot` -> `openai-codex` -> `openrouter`),
|
||||
- token sourcing from `gh auth token` at runtime only when `copilot` is selected.
|
||||
- `tools/ai/zeroclaw_stack_up.sh`
|
||||
- starts local gateway and local follow server,
|
||||
- reuses existing listeners when ports are already bound (prevents duplicate-start failures),
|
||||
- loads local auth env file `~/.zeroclaw/env` when present,
|
||||
- attempts automatic gateway pairing and token refresh,
|
||||
- validates bearer token with a malformed webhook probe (`{}` payload) and re-pairs when possible,
|
||||
- generates live follow dashboard at `http://127.0.0.1:8788/`,
|
||||
- dashboard includes live polling panels for `/conversations.jsonl` and `/gateway.log` (1s polling),
|
||||
- preserves direct raw links: `/conversations.jsonl` and `/gateway.log`,
|
||||
- writes `artifacts/zeroclaw/prometheus.yml` scrape config,
|
||||
- supports local Prometheus startup via `ZEROCLAW_PROM_MODE` (`off`, `auto`, `binary`, `docker`),
|
||||
- supports local Prometheus startup via `ZEROCLAW_PROM_MODE` (`off`, `auto`, `binary`, `docker`) with `auto` fallback `binary -> docker`,
|
||||
- on macOS, auto-attempts Docker Desktop startup before Prometheus docker mode,
|
||||
- stores pair token in `artifacts/zeroclaw/pair_token.txt`.
|
||||
- `tools/ai/zeroclaw_stack_down.sh`
|
||||
- stops local gateway/follow processes,
|
||||
- stops local Prometheus process/container if managed by the stack,
|
||||
- confirms logs remain in `artifacts/zeroclaw/`.
|
||||
- `tools/ai/zeroclaw_webhook_send.sh`
|
||||
- requires `--allow-model-call` before any real webhook send,
|
||||
- sends webhook by default (no mandatory allow flag),
|
||||
- keeps `--allow-model-call` as backward-compatible legacy option,
|
||||
- supports `--dry-run` to validate payload/limits without network send,
|
||||
- enforces autonomous local quotas with `artifacts/zeroclaw/webhook_budget.json`,
|
||||
- supports `--repo-hint <hint>` metadata tagging,
|
||||
- appends enriched JSONL traces to `artifacts/zeroclaw/conversations.jsonl`.
|
||||
|
||||
@@ -155,22 +163,43 @@ Compatibility rule:
|
||||
|
||||
- viewer tolerates legacy lines that only contain `ts`, `message`, `response_raw`.
|
||||
|
||||
Credit-protection rule:
|
||||
Webhook execution and cost controls:
|
||||
|
||||
- without `--allow-model-call`, `zeroclaw_webhook_send.sh` exits non-zero and does not send/write logs.
|
||||
- webhook send is enabled by default.
|
||||
- `--dry-run` performs validation only (no network call, no execution log append).
|
||||
- hourly quota and message-size limits are enforced by environment variables:
|
||||
- `ZEROCLAW_WEBHOOK_MAX_CALLS_PER_HOUR` (default: `40`)
|
||||
- `ZEROCLAW_WEBHOOK_MAX_CHARS` (default: `1200`)
|
||||
- quota state is stored in `artifacts/zeroclaw/webhook_budget.json`.
|
||||
|
||||
## 9) Prometheus Integration
|
||||
|
||||
Economical default:
|
||||
|
||||
- `ZEROCLAW_PROM_MODE=auto` (start only if local `prometheus` binary exists)
|
||||
- `ZEROCLAW_PROM_MODE=auto` (try local `prometheus` binary first, then Docker fallback)
|
||||
|
||||
Optional modes:
|
||||
|
||||
- `ZEROCLAW_PROM_MODE=off` disables Prometheus startup
|
||||
- `ZEROCLAW_PROM_MODE=binary` requires local `prometheus` binary
|
||||
- `ZEROCLAW_PROM_MODE=docker` runs `prom/prometheus:latest` locally
|
||||
- `ZEROCLAW_DOCKER_WAIT_SECS` controls Docker daemon wait timeout (default: `90`)
|
||||
- `ZEROCLAW_PROM_READY_WAIT_SECS` controls Prometheus readiness wait (default: `15`)
|
||||
|
||||
Default local endpoint when running:
|
||||
|
||||
- `http://127.0.0.1:9090/targets`
|
||||
|
||||
## 10) Local Auth Bootstrap
|
||||
|
||||
Recommended local secret file:
|
||||
|
||||
- `~/.zeroclaw/env` (permissions `600`)
|
||||
|
||||
Suggested contents:
|
||||
|
||||
- `OPENROUTER_API_KEY=...`
|
||||
|
||||
Behavior:
|
||||
|
||||
- `zeroclaw_dual_chat.sh`, `zeroclaw_stack_up.sh`, and `zeroclaw_webhook_send.sh` auto-load this file if present.
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
load_local_env() {
|
||||
local env_file="${ZEROCLAW_ENV_FILE:-$HOME/.zeroclaw/env}"
|
||||
[[ -f "$env_file" ]] || return 0
|
||||
chmod 600 "$env_file" >/dev/null 2>&1 || true
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$env_file"
|
||||
set +a
|
||||
}
|
||||
|
||||
load_local_env
|
||||
|
||||
ZEROCLAW_BIN="${ZEROCLAW_BIN:-/Users/cils/Documents/Lelectron_rare/Kill_LIFE/zeroclaw/target/release/zeroclaw}"
|
||||
RTC_REPO="/Users/cils/Documents/Lelectron_rare/RTC_BL_PHONE"
|
||||
ZACUS_REPO="/Users/cils/Documents/Lelectron_rare/le-mystere-professeur-zacus"
|
||||
|
||||
+166
-23
@@ -1,6 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
load_local_env() {
|
||||
local env_file="${ZEROCLAW_ENV_FILE:-$HOME/.zeroclaw/env}"
|
||||
[[ -f "$env_file" ]] || return 0
|
||||
chmod 600 "$env_file" >/dev/null 2>&1 || true
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$env_file"
|
||||
set +a
|
||||
}
|
||||
|
||||
load_local_env
|
||||
|
||||
ROOT_DIR="/Users/cils/Documents/Lelectron_rare/Kill_LIFE"
|
||||
ART_DIR="${ZEROCLAW_ART_DIR:-$ROOT_DIR/artifacts/zeroclaw}"
|
||||
ZEROCLAW_BIN="${ZEROCLAW_BIN:-$ROOT_DIR/zeroclaw/target/release/zeroclaw}"
|
||||
@@ -13,6 +25,8 @@ PROM_PORT="${ZEROCLAW_PROM_PORT:-9090}"
|
||||
PROM_RETENTION="${ZEROCLAW_PROM_RETENTION:-24h}"
|
||||
PROM_SCRAPE_INTERVAL="${ZEROCLAW_PROM_SCRAPE_INTERVAL:-15s}"
|
||||
PROM_CONTAINER="${ZEROCLAW_PROM_CONTAINER:-zeroclaw-prometheus}"
|
||||
DOCKER_WAIT_SECS="${ZEROCLAW_DOCKER_WAIT_SECS:-90}"
|
||||
PROM_READY_WAIT_SECS="${ZEROCLAW_PROM_READY_WAIT_SECS:-15}"
|
||||
|
||||
GW_PID_FILE="$ART_DIR/gateway.pid"
|
||||
FW_PID_FILE="$ART_DIR/follow.pid"
|
||||
@@ -61,6 +75,41 @@ listener_pid_on_port() {
|
||||
lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null | head -n 1
|
||||
}
|
||||
|
||||
ensure_docker_daemon() {
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
if docker info >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
open -ga Docker >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
local waited=0
|
||||
while (( waited < DOCKER_WAIT_SECS )); do
|
||||
if docker info >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
waited=$((waited + 2))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_prometheus_ready() {
|
||||
local waited=0
|
||||
while (( waited < PROM_READY_WAIT_SECS )); do
|
||||
if curl -fsS "http://$PROM_HOST:$PROM_PORT/-/ready" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
write_prometheus_config() {
|
||||
mkdir -p "$PROM_DATA_DIR"
|
||||
cat >"$PROM_CONFIG_FILE" <<EOF
|
||||
@@ -78,7 +127,7 @@ EOF
|
||||
|
||||
start_prometheus_binary() {
|
||||
if is_running "$PROM_PID_FILE"; then
|
||||
PROM_STATUS="binary(pid $(cat "$PROM_PID_FILE"))"
|
||||
PROM_STATUS="binary(existing pid $(cat "$PROM_PID_FILE"))"
|
||||
return 0
|
||||
fi
|
||||
if ! command -v prometheus >/dev/null 2>&1; then
|
||||
@@ -94,7 +143,11 @@ start_prometheus_binary() {
|
||||
sleep 0.3
|
||||
if is_running "$PROM_PID_FILE"; then
|
||||
printf '%s\n' "binary" >"$PROM_MANAGED_FILE"
|
||||
PROM_STATUS="binary"
|
||||
if wait_for_prometheus_ready; then
|
||||
PROM_STATUS="binary(ready)"
|
||||
else
|
||||
PROM_STATUS="binary(starting)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
rm -f "$PROM_PID_FILE"
|
||||
@@ -105,14 +158,18 @@ start_prometheus_docker() {
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
if ! ensure_docker_daemon; then
|
||||
return 1
|
||||
fi
|
||||
local running_id
|
||||
running_id="$(docker ps --filter "name=^/${PROM_CONTAINER}$" --format '{{.ID}}' | head -n 1)"
|
||||
if [[ -n "$running_id" ]]; then
|
||||
printf '%s\n' "docker" >"$PROM_MANAGED_FILE"
|
||||
PROM_STATUS="docker(existing:$PROM_CONTAINER)"
|
||||
if wait_for_prometheus_ready; then
|
||||
PROM_STATUS="docker(existing:$PROM_CONTAINER,ready)"
|
||||
else
|
||||
PROM_STATUS="docker(existing:$PROM_CONTAINER,starting)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -133,12 +190,100 @@ start_prometheus_docker() {
|
||||
--web.listen-address=:9090 2>>"$PROM_LOG" || true)"
|
||||
if [[ -n "$container_id" ]]; then
|
||||
printf '%s\n' "docker" >"$PROM_MANAGED_FILE"
|
||||
PROM_STATUS="docker($PROM_CONTAINER)"
|
||||
if wait_for_prometheus_ready; then
|
||||
PROM_STATUS="docker($PROM_CONTAINER,ready)"
|
||||
else
|
||||
PROM_STATUS="docker($PROM_CONTAINER,starting)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
gateway_is_paired() {
|
||||
curl -fsS "http://$GATEWAY_HOST:$GATEWAY_PORT/health" 2>/dev/null | python3 -c 'import json,sys
|
||||
raw=sys.stdin.read().strip()
|
||||
try:
|
||||
obj=json.loads(raw) if raw else {}
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
sys.exit(0 if obj.get("paired") else 1)' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
read_pair_token() {
|
||||
[[ -f "$TOKEN_FILE" ]] || return 1
|
||||
cat "$TOKEN_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
token_is_usable() {
|
||||
local token="${1:-}"
|
||||
[[ -n "$token" ]] || return 1
|
||||
local http_status
|
||||
http_status="$(curl -sS -o /dev/null -w "%{http_code}" -X POST "http://$GATEWAY_HOST:$GATEWAY_PORT/webhook" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}' || true)"
|
||||
|
||||
# 400/422 typically means auth passed but payload was intentionally malformed.
|
||||
if [[ "$http_status" == "401" || "$http_status" == "403" || "$http_status" == "000" ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
extract_pair_code() {
|
||||
grep -Eo 'X-Pairing-Code: [0-9]{6}' "$GW_LOG" 2>/dev/null | tail -1 | awk '{print $2}' || true
|
||||
}
|
||||
|
||||
write_pair_token_from_code() {
|
||||
local pair_code="$1"
|
||||
local pair_json token
|
||||
pair_json="$(curl -sS -X POST "http://$GATEWAY_HOST:$GATEWAY_PORT/pair" -H "X-Pairing-Code: $pair_code" || true)"
|
||||
token="$(printf '%s' "$pair_json" | python3 -c 'import json,sys
|
||||
raw=sys.stdin.read().strip()
|
||||
try:
|
||||
obj=json.loads(raw) if raw else {}
|
||||
except Exception:
|
||||
obj={}
|
||||
print(obj.get("token",""), end="")')"
|
||||
if [[ -z "$token" ]]; then
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$token" >"$TOKEN_FILE"
|
||||
chmod 600 "$TOKEN_FILE"
|
||||
return 0
|
||||
}
|
||||
|
||||
ensure_gateway_pairing() {
|
||||
local current_token=""
|
||||
current_token="$(read_pair_token || true)"
|
||||
if gateway_is_paired && token_is_usable "$current_token"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local pair_code
|
||||
pair_code="$(extract_pair_code)"
|
||||
if [[ -n "$pair_code" ]]; then
|
||||
write_pair_token_from_code "$pair_code" || true
|
||||
fi
|
||||
|
||||
current_token="$(read_pair_token || true)"
|
||||
if gateway_is_paired && token_is_usable "$current_token"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep 0.5
|
||||
pair_code="$(extract_pair_code)"
|
||||
if [[ -n "$pair_code" ]]; then
|
||||
write_pair_token_from_code "$pair_code" || true
|
||||
fi
|
||||
|
||||
current_token="$(read_pair_token || true)"
|
||||
if ! token_is_usable "$current_token"; then
|
||||
rm -f "$TOKEN_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
if is_running "$GW_PID_FILE"; then
|
||||
echo "[info] gateway already running (pid $(cat "$GW_PID_FILE"))."
|
||||
elif [[ -n "$(listener_pid_on_port "$GATEWAY_PORT")" ]]; then
|
||||
@@ -172,21 +317,7 @@ for _ in $(seq 1 40); do
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
PAIR_CODE="$(grep -Eo 'X-Pairing-Code: [0-9]{6}' "$GW_LOG" 2>/dev/null | tail -1 | awk '{print $2}' || true)"
|
||||
if [[ -n "$PAIR_CODE" ]]; then
|
||||
PAIR_JSON="$(curl -sS -X POST "http://$GATEWAY_HOST:$GATEWAY_PORT/pair" -H "X-Pairing-Code: $PAIR_CODE" || true)"
|
||||
TOKEN="$(printf '%s' "$PAIR_JSON" | python3 -c 'import json,sys
|
||||
raw=sys.stdin.read().strip()
|
||||
try:
|
||||
obj=json.loads(raw) if raw else {}
|
||||
except Exception:
|
||||
obj={}
|
||||
print(obj.get("token",""), end="")')"
|
||||
if [[ -n "$TOKEN" ]]; then
|
||||
printf '%s\n' "$TOKEN" >"$TOKEN_FILE"
|
||||
chmod 600 "$TOKEN_FILE"
|
||||
fi
|
||||
fi
|
||||
ensure_gateway_pairing
|
||||
|
||||
write_prometheus_config
|
||||
case "$PROM_MODE" in
|
||||
@@ -196,8 +327,12 @@ case "$PROM_MODE" in
|
||||
;;
|
||||
auto)
|
||||
rm -f "$PROM_MANAGED_FILE"
|
||||
if ! start_prometheus_binary; then
|
||||
PROM_STATUS="disabled(prometheus missing; use ZEROCLAW_PROM_MODE=docker)"
|
||||
if start_prometheus_binary; then
|
||||
:
|
||||
elif start_prometheus_docker; then
|
||||
:
|
||||
else
|
||||
PROM_STATUS="disabled(no prometheus backend available)"
|
||||
fi
|
||||
;;
|
||||
binary)
|
||||
@@ -209,7 +344,7 @@ case "$PROM_MODE" in
|
||||
docker)
|
||||
rm -f "$PROM_MANAGED_FILE"
|
||||
if ! start_prometheus_docker; then
|
||||
PROM_STATUS="disabled(docker unavailable)"
|
||||
PROM_STATUS="disabled(docker unavailable or daemon not ready)"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
@@ -443,5 +578,13 @@ EOF
|
||||
|
||||
echo "Gateway: http://$GATEWAY_HOST:$GATEWAY_PORT/health"
|
||||
echo "Follow : http://127.0.0.1:$FOLLOW_PORT/"
|
||||
echo "Prom : http://$PROM_HOST:$PROM_PORT/targets ($PROM_STATUS)"
|
||||
echo "Logs : $GW_LOG"
|
||||
echo "Token : $TOKEN_FILE"
|
||||
if [[ ! -s "$TOKEN_FILE" ]]; then
|
||||
if gateway_is_paired; then
|
||||
echo "[warn] gateway is paired but local bearer token is unavailable; webhook_send may require ZEROCLAW_BEARER or gateway re-pair."
|
||||
else
|
||||
echo "[warn] pair token not found. If gateway is waiting for pairing, restart and pair again."
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
load_local_env() {
|
||||
local env_file="${ZEROCLAW_ENV_FILE:-$HOME/.zeroclaw/env}"
|
||||
[[ -f "$env_file" ]] || return 0
|
||||
chmod 600 "$env_file" >/dev/null 2>&1 || true
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$env_file"
|
||||
set +a
|
||||
}
|
||||
|
||||
load_local_env
|
||||
|
||||
ART_DIR="${ZEROCLAW_ART_DIR:-/Users/cils/Documents/Lelectron_rare/Kill_LIFE/artifacts/zeroclaw}"
|
||||
HOST="${ZEROCLAW_GATEWAY_HOST:-127.0.0.1}"
|
||||
PORT="${ZEROCLAW_GATEWAY_PORT:-3000}"
|
||||
TOKEN_FILE="$ART_DIR/pair_token.txt"
|
||||
CONVO_FILE="$ART_DIR/conversations.jsonl"
|
||||
BUDGET_FILE="$ART_DIR/webhook_budget.json"
|
||||
MAX_CALLS_PER_HOUR="${ZEROCLAW_WEBHOOK_MAX_CALLS_PER_HOUR:-40}"
|
||||
MAX_CHARS="${ZEROCLAW_WEBHOOK_MAX_CHARS:-1200}"
|
||||
|
||||
usage() {
|
||||
cat >&2 <<USAGE
|
||||
Usage:
|
||||
$(basename "$0") [--repo-hint <hint>] [--allow-model-call] "message"
|
||||
$(basename "$0") [--repo-hint <hint>] [--allow-model-call] [--dry-run] "message"
|
||||
|
||||
Options:
|
||||
--repo-hint <hint> conversation source hint (e.g. rtc, zacus)
|
||||
--allow-model-call required to actually send webhook requests
|
||||
--allow-model-call legacy option (no longer required)
|
||||
--dry-run validate payload and limits without network send
|
||||
-h, --help show this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
REPO_HINT="unknown"
|
||||
ALLOW_MODEL_CALL=0
|
||||
DRY_RUN=0
|
||||
MESSAGE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -38,6 +55,10 @@ while [[ $# -gt 0 ]]; do
|
||||
ALLOW_MODEL_CALL=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
@@ -64,22 +85,79 @@ if [[ -z "$MESSAGE" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ALLOW_MODEL_CALL" -ne 1 ]]; then
|
||||
echo "[guard] dry guard / credit protection: add --allow-model-call to send." >&2
|
||||
exit 10
|
||||
if [[ "$ALLOW_MODEL_CALL" -eq 1 ]]; then
|
||||
echo "[info] --allow-model-call accepted (legacy no-op)." >&2
|
||||
fi
|
||||
|
||||
TOKEN="${ZEROCLAW_BEARER:-}"
|
||||
if [[ -z "$TOKEN" && -f "$TOKEN_FILE" ]]; then
|
||||
TOKEN="$(cat "$TOKEN_FILE")"
|
||||
fi
|
||||
|
||||
mkdir -p "$ART_DIR"
|
||||
touch "$CONVO_FILE"
|
||||
|
||||
MESSAGE_CHARS="$(python3 -c 'import sys;print(len(sys.argv[1]))' "$MESSAGE")"
|
||||
if (( MESSAGE_CHARS > MAX_CHARS )); then
|
||||
echo "[budget] message length ${MESSAGE_CHARS} exceeds ZEROCLAW_WEBHOOK_MAX_CHARS=${MAX_CHARS}." >&2
|
||||
exit 11
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo "[dry-run] payload validated; no network call performed."
|
||||
echo "[dry-run] repo_hint=$REPO_HINT chars=$MESSAGE_CHARS"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
echo "[fail] no bearer token found. Start stack first or set ZEROCLAW_BEARER." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$ART_DIR"
|
||||
touch "$CONVO_FILE"
|
||||
if ! python3 - "$BUDGET_FILE" "$MAX_CALLS_PER_HOUR" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
path = sys.argv[1]
|
||||
max_calls = int(sys.argv[2])
|
||||
now = int(time.time())
|
||||
window_start = now - 3600
|
||||
|
||||
state = {"calls": []}
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
loaded = json.load(f)
|
||||
if isinstance(loaded, dict):
|
||||
state = loaded
|
||||
except Exception:
|
||||
state = {"calls": []}
|
||||
|
||||
calls = []
|
||||
for item in state.get("calls", []):
|
||||
try:
|
||||
ts = int(float(item))
|
||||
except Exception:
|
||||
continue
|
||||
if ts >= window_start:
|
||||
calls.append(ts)
|
||||
|
||||
if max_calls > 0 and len(calls) >= max_calls:
|
||||
print(f"[budget] hourly call limit reached: {len(calls)}/{max_calls}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
calls.append(now)
|
||||
tmp_path = path + ".tmp"
|
||||
with open(tmp_path, "w", encoding="utf-8") as f:
|
||||
json.dump({"calls": calls}, f, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp_path, path)
|
||||
print(f"[budget] calls in last hour: {len(calls)}/{max_calls}", file=sys.stderr)
|
||||
PY
|
||||
then
|
||||
exit 12
|
||||
fi
|
||||
|
||||
PAYLOAD="$(python3 -c 'import json,sys;print(json.dumps({"message":sys.argv[1]}))' "$MESSAGE")"
|
||||
TMP_BODY="$(mktemp)"
|
||||
|
||||
Reference in New Issue
Block a user