150 lines
4.7 KiB
Bash
150 lines
4.7 KiB
Bash
|
|
#!/usr/bin/env bash
|
|||
|
|
set -euo pipefail
|
|||
|
|
|
|||
|
|
SKILL_DIR="/home/openclaw/.openclaw/workspace/skills/codex-account-switcher-skill"
|
|||
|
|
cd "$SKILL_DIR"
|
|||
|
|
|
|||
|
|
before_json="$(./codex-accounts.py list --json 2>/dev/null || echo '{}')"
|
|||
|
|
auto_json="$(./codex-accounts.py auto --json 2>/dev/null || echo '{}')"
|
|||
|
|
after_json="$(./codex-accounts.py list --json 2>/dev/null || echo '{}')"
|
|||
|
|
|
|||
|
|
python3 - "$before_json" "$auto_json" "$after_json" <<'PY'
|
|||
|
|
import json
|
|||
|
|
import subprocess
|
|||
|
|
import sys
|
|||
|
|
from datetime import datetime, timezone, timedelta
|
|||
|
|
|
|||
|
|
before_s, auto_s, after_s = sys.argv[1:4]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def j(s):
|
|||
|
|
try:
|
|||
|
|
return json.loads(s)
|
|||
|
|
except Exception:
|
|||
|
|
return {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
before = j(before_s)
|
|||
|
|
auto = j(auto_s)
|
|||
|
|
after = j(after_s)
|
|||
|
|
|
|||
|
|
MSK = timezone(timedelta(hours=3))
|
|||
|
|
now = datetime.now(MSK).strftime('%Y-%m-%d %H:%M:%S MSK')
|
|||
|
|
|
|||
|
|
accounts = [a.get('name') for a in after.get('accounts', []) if a.get('name') and not str(a.get('name')).startswith('.')]
|
|||
|
|
old_name = before.get('active')
|
|||
|
|
new_name = auto.get('switched_to') or after.get('active')
|
|||
|
|
all_acc = auto.get('all_accounts', {}) if isinstance(auto.get('all_accounts', {}), dict) else {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def account_ttl(data, name):
|
|||
|
|
for acc in data.get('accounts', []) if isinstance(data.get('accounts'), list) else []:
|
|||
|
|
if acc.get('name') == name:
|
|||
|
|
ttl = acc.get('token_ttl_seconds')
|
|||
|
|
if isinstance(ttl, (int, float)):
|
|||
|
|
return int(ttl)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fmt_ttl(seconds):
|
|||
|
|
if seconds is None:
|
|||
|
|
return 'ttl н/д'
|
|||
|
|
if seconds <= 0:
|
|||
|
|
return 'ttl expired'
|
|||
|
|
m = seconds // 60
|
|||
|
|
if m < 60:
|
|||
|
|
return f'ttl {m}m'
|
|||
|
|
h = m // 60
|
|||
|
|
rm = m % 60
|
|||
|
|
return f'ttl {h}h{rm:02d}m'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def auth_probe(name):
|
|||
|
|
try:
|
|||
|
|
subprocess.run(['./codex-accounts.py', 'use', name], check=False, capture_output=True, text=True, timeout=15)
|
|||
|
|
p = subprocess.run(['codex', 'exec', 'PING', '--skip-git-repo-check', '--json'], check=False, capture_output=True, text=True, timeout=60)
|
|||
|
|
return 'ok' if p.returncode == 0 else 'fail'
|
|||
|
|
except Exception:
|
|||
|
|
return 'fail'
|
|||
|
|
|
|||
|
|
|
|||
|
|
probe = {}
|
|||
|
|
for n in accounts:
|
|||
|
|
probe[n] = auth_probe(n)
|
|||
|
|
|
|||
|
|
# Restore active account after probes
|
|||
|
|
restore = new_name or old_name
|
|||
|
|
if restore:
|
|||
|
|
subprocess.run(['./codex-accounts.py', 'use', restore], check=False, capture_output=True, text=True, timeout=15)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fmt_acc(name):
|
|||
|
|
d = all_acc.get(name, {}) if name else {}
|
|||
|
|
auth = probe.get(name, 'н/д') if name else 'н/д'
|
|||
|
|
|
|||
|
|
if d and 'error' not in d:
|
|||
|
|
w = d.get('weekly_used')
|
|||
|
|
a = d.get('available')
|
|||
|
|
dd = d.get('daily_used')
|
|||
|
|
if w is not None and a is not None:
|
|||
|
|
return f"auth={auth}, week used={w:.0f}%, week left={a:.0f}%, day used={dd:.0f}%"
|
|||
|
|
|
|||
|
|
ttl_after = account_ttl(after, name)
|
|||
|
|
ttl_before = account_ttl(before, name)
|
|||
|
|
ttl = ttl_after if ttl_after is not None else ttl_before
|
|||
|
|
if d and 'error' in d:
|
|||
|
|
return f"auth={auth}, лимит: {d['error']}, {fmt_ttl(ttl)}"
|
|||
|
|
return f"auth={auth}, {fmt_ttl(ttl)}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
if auto.get('error'):
|
|||
|
|
status = f"quota-check failed: {auto.get('error')}"
|
|||
|
|
else:
|
|||
|
|
status = 'ok'
|
|||
|
|
|
|||
|
|
switch_note = 'без переключения'
|
|||
|
|
if old_name and new_name and old_name != new_name:
|
|||
|
|
switch_note = f"переключение: {old_name} -> {new_name}"
|
|||
|
|
elif new_name:
|
|||
|
|
switch_note = f"активный аккаунт: {new_name}"
|
|||
|
|
|
|||
|
|
accounts_summary = ', '.join([f"{n}:{probe.get(n, 'н/д')}" for n in accounts]) if accounts else 'н/д'
|
|||
|
|
|
|||
|
|
def reset_eta(epoch):
|
|||
|
|
try:
|
|||
|
|
if not epoch:
|
|||
|
|
return 'н/д'
|
|||
|
|
dt = datetime.fromtimestamp(int(epoch), tz=MSK)
|
|||
|
|
return dt.strftime('%Y-%m-%d %H:%M MSK')
|
|||
|
|
except Exception:
|
|||
|
|
return 'н/д'
|
|||
|
|
|
|||
|
|
lines = []
|
|||
|
|
lines.append(f"📊 Codex quota report — {now}")
|
|||
|
|
lines.append(f"Статус: {status}")
|
|||
|
|
lines.append(f"Маршрут: {switch_note}")
|
|||
|
|
lines.append("")
|
|||
|
|
lines.append("Аккаунты:")
|
|||
|
|
|
|||
|
|
for name in accounts:
|
|||
|
|
d = all_acc.get(name, {})
|
|||
|
|
auth = probe.get(name, 'н/д')
|
|||
|
|
ttl = account_ttl(after, name)
|
|||
|
|
ttl_text = fmt_ttl(ttl)
|
|||
|
|
if d and 'error' not in d and d.get('weekly_used') is not None:
|
|||
|
|
lines.append(
|
|||
|
|
f"- {name}: auth={auth}, за неделю использовано {d['weekly_used']:.0f}% / осталось {d['available']:.0f}%, "
|
|||
|
|
f"за день использовано {d.get('daily_used', 0):.0f}%, сброс лимита {reset_eta(d.get('weekly_resets_at'))}, {ttl_text}"
|
|||
|
|
)
|
|||
|
|
elif d and 'error' in d:
|
|||
|
|
lines.append(f"- {name}: auth={auth}, лимит: {d['error']}, {ttl_text}")
|
|||
|
|
else:
|
|||
|
|
lines.append(f"- {name}: auth={auth}, {ttl_text}")
|
|||
|
|
|
|||
|
|
if new_name:
|
|||
|
|
lines.append("")
|
|||
|
|
lines.append(f"✅ Рекомендуемый/активный: {new_name}")
|
|||
|
|
|
|||
|
|
print("\n".join(lines))
|
|||
|
|
PY
|