Initial commit: grok-free-register-oss

Open-source Grok free registration CLI, xai_enroller auth pipeline,
local auth service, tests and docs.
This commit is contained in:
chaos
2026-07-16 21:04:05 +08:00
commit d10009d639
72 changed files with 18752 additions and 0 deletions

944
keys/acpa_watchdog.py Executable file
View File

@@ -0,0 +1,944 @@
#!/usr/bin/env python3
"""
acpa_watchdog.py — 自动 CPA 凭据整备驻守进程
职责:
1. 盯 xai_enroller (auth-service.sh) 的出货目录:
$XAI_ENROLLER_LOCAL_AUTH_DIR/authenticated/xai-*.json
默认 ~/Downloads/grok-free-register-auth/authenticated/
xai_enroller 产出的 CPA 文件 base_url=api.x.ai/v1 (付费口, 免费号必 402)
+ headers=None (缺 grok-cli 身份头) + email=None。
2. 每个新号原样不动地「整备」一份到 PROJECT/keys/cpa_ready/xai-*.json:
- base_url → https://cli-chat-proxy.grok.com/v1 (免费 Grok 4.5 口)
- headers → grok-cli 身份头(x-grok-client-version/identifier 等)
- email → 从 id_token /access_token JWT 里挖(便于辨识, 不影响通信用)
- 权限 0600 (合规: 含 refresh_token)
3. 探活: 打 cli-chat-proxy /v1/responses model=grok-4.5
请求对齐 new-api grok-cli 完整指纹session 族头 + input 数组 + max_output_tokens 极小)。
首探 ≤MAX_PROBES(默认 1省 free 2M 额度)200 → alive。
新号落盘后 ACPA_PROBE_WARMUP_SEC(默认 3s) 再探,避免 mint 后瞬时 permission-denied。
单次探针内 403 当场短重试 ACPA_403_IMMEDIATE_RETRIES×SLEEP默认 2×4s
仍 403 → chat_denied不丢RETEST_AFTER_SEC(默认 180s) 后回测 1 次;
回测活 → alive仍 403 → chat_dead 丢。
429 free-usage-exhausted / rate_limited / 401 → 立刻丢。
alive 永不复探。
4. 状态: keys/cpa_ready/_state.tsv
name\tsub\temail\tstatus\tts\tprobes
每轮 prunejson 在 cpa_ready/_discarded 都不存在的行直接从 TSV 删掉
(删库跑路后不会残留幽灵 alive盘上有 json 但无 state 会重新进探活)。
只读 AUTH_DIR, 只写 keys/cpa_ready/。源 CPA 文件不动; 不重铸 device flow;
不做 HTTP 灌库 (入库步骤留给灌库脚本, 此处只把号整备到可直接喂 cliproxy / new-api)。
CLI:
python3 acpa_watchdog.py # 常驻轮询 (默认 2s), Ctrl-C 退
python3 acpa_watchdog.py --once # 单次扫一轮现存号就退 (CLI 自检)
python3 acpa_watchdog.py --interval 4
"""
from __future__ import annotations
import argparse
import base64
import copy
import json
import os
import signal
import sys
import time
from pathlib import Path
# ---------------------------------------------------------------------------
# paths — portable: follow project root of this script; AUTH overridable via env
# ---------------------------------------------------------------------------
PROJECT_ROOT = Path(__file__).resolve().parent.parent # .../grok-free-register
# Align with xai_enroller.service DEFAULT_LOCAL_AUTH_DIR
# (~/Downloads/grok-free-register-auth/authenticated by default)
_AUTH_BASE = Path(
os.environ.get(
"XAI_ENROLLER_LOCAL_AUTH_DIR",
os.environ.get(
"XAI_AUTH_SERVICE_LOCAL_DIR",
str(Path.home() / "Downloads" / "grok-free-register-auth"),
),
)
).expanduser()
AUTH_DIR = _AUTH_BASE / "authenticated"
OUT_DIR = PROJECT_ROOT / "keys" / "cpa_ready"
DISCARD_DIR = OUT_DIR / "_discarded"
STATE_FILE = OUT_DIR / "_state.tsv"
# grok-cli 静态身份头(对齐 new-api common.GrokCLI* / SetupRequestHeader
# session/req/agent 等 per-request 头只在探针里生成,不写进落盘 json。
CLIPROXY_BASE_URL = "https://cli-chat-proxy.grok.com/v1"
CLIPROXY_HEADERS = {
"x-grok-client-version": "0.2.93",
"x-xai-token-auth": "xai-grok-cli",
"X-XAI-Token-Auth": "xai-grok-cli",
"x-authenticateresponse": "authenticate-response",
"x-grok-client-identifier": "grok-shell",
"x-compaction-at": "400000",
"User-Agent": "grok-shell/0.2.93 (linux; x86_64)",
}
RUN = True
def _sig(signum, _frame):
global RUN
RUN = False
signal.signal(signal.SIGINT, _sig)
signal.signal(signal.SIGTERM, _sig)
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
def log(msg: str) -> None:
print(f"[acpa {time.strftime('%H:%M:%S')}] {msg}", flush=True)
def b64url_decode(seg: str) -> bytes:
seg += "=" * (-len(seg) % 4)
return base64.urlsafe_b64decode(seg)
def jwt_payload(token: str) -> dict:
if not token or token.count(".") != 2:
return {}
try:
return json.loads(b64url_decode(token.split(".")[1]))
except Exception:
return {}
def load_source(path: Path) -> dict | None:
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception as exc:
log(f" ✗ 读 {path.name} 失败: {exc}")
return None
def ensure_dir(p: Path) -> None:
p.mkdir(parents=True, exist_ok=True)
try:
os.chmod(p, 0o700)
except OSError:
pass
def fallback_email(src: dict) -> str:
"""xai_enroller 产的 file email 字段是 None。从 JWT 里挖一个标识。"""
for key in ("id_token", "access_token"):
pl = jwt_payload(src.get(key) or "")
for f in ("email", "preferred_username", "sub"):
v = pl.get(f)
if v:
return str(v)
return ""
def finalize(src: dict) -> tuple[str, dict]:
"""把 xai_enroller 产的 raw CPA 整备成 cli-chat-proxy 可用格式。返回 (sub, dst)。"""
dst = copy.deepcopy(src)
dst["base_url"] = CLIPROXY_BASE_URL
dst["headers"] = dict(CLIPROXY_HEADERS)
# token_endpoint 不变 (auth.x.ai/oauth2/token, refresh 用)
dst.setdefault("auth_kind", "oauth")
dst.setdefault("type", "xai")
if not dst.get("email"):
dst["email"] = fallback_email(src)
sub = dst.get("sub") or jwt_payload(dst.get("access_token", "")).get("sub", "")
dst["sub"] = sub
return sub, dst
def write_out(name: str, entry: dict) -> Path:
ensure_dir(OUT_DIR)
target = OUT_DIR / name
tmp = target.with_suffix(target.suffix + ".tmp")
tmp.write_text(json.dumps(entry, separators=(",", ":"), ensure_ascii=False),
encoding="utf-8")
os.replace(tmp, target)
os.chmod(target, 0o600)
return target
# ---------------------------------------------------------------------------
# probe liveness — 使用项目自带 .venv(已装 curl_cffi)
# 完整 grok-cli 指纹;默认每号只首探 1 次free ~2M禁止连 ping
# ---------------------------------------------------------------------------
_VENV_PY = PROJECT_ROOT / ".venv" / "bin" / "python"
_PROBE_SRC = r'''
import json, sys, uuid
try:
from curl_cffi import requests
except Exception as e:
print("NO_CURL_CFFI", str(e)); sys.exit(3)
fp = sys.argv[1]
d = json.load(open(fp))
at = d.get("access_token") or ""
if not at:
print(json.dumps({"status": "token_dead", "http": 401, "snippet": "no access_token"}))
sys.exit(0)
# 对齐 new-api-fusion relay/channel/grok_cli SetupRequestHeader + common.GrokCLI*
sid = str(uuid.uuid4())
rid = str(uuid.uuid4())
hdrs = {
"Authorization": "Bearer " + at,
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "grok-shell/0.2.93 (linux; x86_64)",
"x-xai-token-auth": "xai-grok-cli",
"X-XAI-Token-Auth": "xai-grok-cli",
"x-grok-client-identifier": "grok-shell",
"x-grok-client-version": "0.2.93",
"x-authenticateresponse": "authenticate-response",
"x-compaction-at": "400000",
"Connection": "Keep-Alive",
"x-grok-session-id": sid,
"x-grok-conv-id": sid,
"x-grok-req-id": rid,
"x-grok-turn-idx": "1",
"x-grok-agent-id": "agent-" + rid[:8],
"x-grok-model-override": "grok-4.5",
}
email = d.get("email") or ""
sub = d.get("sub") or ""
if email:
hdrs["x-email"] = str(email)
if sub:
hdrs["x-userid"] = str(sub)
base = (d.get("base_url") or "https://cli-chat-proxy.grok.com/v1").rstrip("/")
# 兼容 base 已带 /v1 或只有 host
if base.endswith("/responses"):
url = base
else:
url = base + "/responses"
# input 必须是 items 数组;裸字符串会被上游拒 / 误 403
# max_output_tokens 压到极小free 号额度只花探针这一枪
body = {
"model": "grok-4.5",
"store": False,
"stream": False,
"max_output_tokens": 16,
"input": [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "ok"}],
}
],
}
try:
r = requests.post(url, json=body, headers=hdrs, impersonate="chrome", timeout=45)
txt = r.text or ""
code = r.status_code
except Exception as e:
print("EXC", str(e)[:200]); sys.exit(2)
status = "unknown"
low = txt.lower()
if code == 200 and ("output" in txt or "completed" in low or '"status"' in low):
# 200 即视为通路可用;不要求模型真回 "ok"(省解码/推理)
status = "alive"
elif (
code == 429
or "free-usage-exhausted" in low
or "used all the include" in low
or "rate limit" in low
or "rate_limit" in low
):
status = "free_exhausted" if ("free-usage" in low or "used all" in low) else "rate_limited"
elif code == 402 or "personal-team-blocked" in txt or "spending-limit" in txt:
status = "no_quota_paid_end"
elif code == 403 or "permission-denied" in txt or "chat endpoint is denied" in low:
status = "chat_denied"
elif code == 401:
status = "token_dead"
elif code == 422:
# body 形态问题:标 unknown 便于发现探针回归,不直接当号死
status = "unknown"
elif "forbidden" in low:
status = "chat_denied"
print(json.dumps({"status": status, "http": code, "snippet": txt[:200]}))
sys.exit(0)
'''
# 省额度:默认首探 1 次403 当场短重试后再标 soft默认 3 分钟后延迟回测 1 次。
# alive 永不复探。429 free-usage-exhausted / token_dead 等真坏号立刻丢。
MAX_PROBES = int(
os.environ.get(
"ACPA_MAX_PROBES",
os.environ.get("ACPA_PROBE_RETRIES", "1"),
)
)
PROBE_RETRY_SLEEP = float(os.environ.get("ACPA_PROBE_RETRY_SLEEP", "1.0"))
# 新号 finalize 后稍等再首探:上游常对刚 mint 的 access 回瞬时 permission-denied
PROBE_WARMUP_SEC = float(os.environ.get("ACPA_PROBE_WARMUP_SEC", "3.0"))
# 单次 probe_one 内 403 当场短重试(不算额外 MAX_PROBES 预算)
IMMEDIATE_403_RETRIES = int(os.environ.get("ACPA_403_IMMEDIATE_RETRIES", "2"))
IMMEDIATE_403_SLEEP = float(os.environ.get("ACPA_403_IMMEDIATE_SLEEP", "4.0"))
RETEST_AFTER_SEC = int(os.environ.get("ACPA_RETEST_AFTER_SEC", "180")) # 3 分钟
RETEST_MAX = int(os.environ.get("ACPA_RETEST_MAX", "1")) # 403 延迟回测次数
# 终态不再探alive 保留;下列坏号 discard
TERMINAL_OK = frozenset({"alive"})
TERMINAL_BAD = frozenset({
"free_exhausted", # 429 免费额度耗尽 → 丢
"rate_limited", # 其它 429 → 丢
"token_dead", # 401
"no_quota_paid_end",
"chat_dead", # 5 分钟回测后仍 403 → 真坏,丢
"discarded",
})
TERMINAL_STATUSES = TERMINAL_OK | TERMINAL_BAD
# 仅这些立刻/确认后丢出 cpa_ready403 首探不在此列)
DISCARD_STATUSES = frozenset({
"free_exhausted",
"rate_limited",
"token_dead",
"no_quota_paid_end",
"chat_dead",
})
# 等 5 分钟回测的软状态(保留 json不进 acc不丢
SOFT_RETEST_STATUSES = frozenset({
"chat_denied",
"forbidden_domain_ban", # 历史误标,按 403 软状态回测
"probe_cap", # 历史:首探 2 次 403 被钉死,捞回可回测
})
def probe_one(path: Path) -> dict:
"""用子进程跑探活(隔离 curl_cffi 环境)。返回 {status, http, snippet}."""
import subprocess
cp = subprocess.run(
[str(_VENV_PY), "-c", _PROBE_SRC, str(path)],
capture_output=True, text=True, timeout=60,
)
out = cp.stdout.strip()
if out.startswith("alive") or out.lstrip().startswith("{"):
try:
idx = out.find("{")
if idx >= 0:
return json.loads(out[idx:])
except Exception:
pass
return {"status": "probe_err", "http": -1, "snippet": (out or cp.stderr[:200])[:200]}
def probe_one_soft(path: Path) -> dict:
"""
一次「逻辑探针」:底层请求 + 403/瞬时错误当场短重试。
仍计为 probe_budgeted 的 1 次 used不因短重试烧额外额度记账
"""
last = {"status": "probe_err", "http": -1, "snippet": ""}
attempts = 1 + max(0, IMMEDIATE_403_RETRIES)
for i in range(attempts):
last = probe_one(path)
st = last.get("status")
if st in (
"alive", "free_exhausted", "rate_limited",
"no_quota_paid_end", "token_dead",
):
return last
if st in ("chat_denied", "forbidden_domain_ban", "probe_err", "unknown"):
if i + 1 < attempts:
time.sleep(IMMEDIATE_403_SLEEP)
continue
break
if last.get("status") == "forbidden_domain_ban":
last = {
"status": "chat_denied",
"http": last.get("http", 403),
"snippet": last.get("snippet", "")[:200],
}
return last
def probe_budgeted(path: Path, budget: int) -> tuple[dict, int]:
"""
在 budget 次内探活。返回 (最后结果, 实际探活次数)。
alive / 额度终态立刻停403 可在预算内连探;最终 403 保持 chat_denied不改 probe_cap
"""
budget = max(0, int(budget))
if budget <= 0:
return {"status": "chat_denied", "http": -1, "snippet": "no probe budget"}, 0
last = {"status": "probe_err", "http": -1, "snippet": ""}
used = 0
for i in range(budget):
last = probe_one_soft(path)
used += 1
st = last.get("status")
if st in (
"alive", "free_exhausted", "rate_limited",
"no_quota_paid_end", "token_dead",
):
return last, used
if i + 1 < budget and st in (
"chat_denied", "forbidden_domain_ban", "probe_err", "unknown",
):
time.sleep(PROBE_RETRY_SLEEP)
continue
break
# 首探/回测结束仍是 403 → 保持 chat_denied交给延迟回测或 chat_dead
if last.get("status") in ("forbidden_domain_ban",):
last = {
"status": "chat_denied",
"http": last.get("http", 403),
"snippet": last.get("snippet", "")[:200],
}
return last, used
# ---------------------------------------------------------------------------
# state — name \t sub \t email \t status \t ts [\t probes]
# ---------------------------------------------------------------------------
def load_state() -> dict:
"""{name: {sub, email, status, ts, probes}}"""
if not STATE_FILE.exists():
return {}
st = {}
for ln in STATE_FILE.read_text(encoding="utf-8").splitlines():
parts = ln.split("\t")
if len(parts) >= 5:
name, sub, email, status, ts = parts[:5]
probes = 0
if len(parts) >= 6:
try:
probes = int(parts[5] or 0)
except ValueError:
probes = 0
# 历史行没有 probes已有终态按已探满处理避免重启后重烧额度
if probes <= 0 and status in TERMINAL_STATUSES | {
"chat_denied", "forbidden_domain_ban", "unknown", "probe_err",
}:
probes = MAX_PROBES if status != "alive" else 1
if probes <= 0 and status == "alive":
probes = 1
st[name] = {
"sub": sub, "email": email, "status": status,
"ts": ts, "probes": probes,
}
return st
def write_state(st: dict) -> None:
ensure_dir(OUT_DIR)
lines = []
for name, v in sorted(st.items()):
lines.append("\t".join([
name, v.get("sub", ""), v.get("email", ""),
v.get("status", ""), str(v.get("ts", "")),
str(int(v.get("probes") or 0)),
]))
tmp = STATE_FILE.with_suffix(".tsv.tmp")
tmp.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
os.replace(tmp, STATE_FILE)
os.chmod(STATE_FILE, 0o600)
def json_exists_for(name: str) -> bool:
"""cpa_ready 主目录或 _discarded 里是否还有该号 json。"""
return (OUT_DIR / name).exists() or (DISCARD_DIR / name).exists()
def prune_orphan_state(st: dict) -> int:
"""
删库跑路 / 手工清 json 后TSV 里既无 cpa_ready 也无 _discarded 文件的行直接 drop。
避免幽灵 alive 把 sync/账本钉死。返回删除行数。
"""
if not st:
return 0
drop = [name for name in list(st.keys()) if not json_exists_for(name)]
if not drop:
return 0
for name in drop:
del st[name]
write_state(st)
log(f" 🧹 prune orphan state {len(drop)} 行(盘上无 json")
return len(drop)
def should_discard(status: str, probes: int = 0) -> bool:
"""仅真坏号丢429 额度耗尽 / 401 / 回测后仍 403(chat_dead)。首探 403 不丢。"""
return status in DISCARD_STATUSES
def discard_bad(name: str, st: dict, *, reason: str = "") -> bool:
"""
坏号移出 keys/cpa_ready/ → keys/cpa_ready/_discarded/
state 标记 discardedsync_acc 只认 alive 故 acc.md 自动剔除。
返回是否执行了丢弃。
"""
entry = st.get(name) or {}
status = entry.get("status", reason or "bad")
probes = _probes(entry) if entry else MAX_PROBES
if status == "discarded":
# 已丢过:确保 json 不在主目录
p = OUT_DIR / name
if p.exists():
ensure_dir(DISCARD_DIR)
dest = DISCARD_DIR / name
try:
os.replace(p, dest)
except OSError:
try:
p.unlink()
except OSError:
pass
return False
if not should_discard(status, probes) and not reason:
return False
ensure_dir(DISCARD_DIR)
src = OUT_DIR / name
moved = False
if src.exists():
dest = DISCARD_DIR / name
try:
if dest.exists():
dest.unlink()
os.replace(src, dest)
os.chmod(dest, 0o600)
moved = True
except OSError as exc:
log(f" ✗ 丢弃移动失败 {name}: {exc}")
try:
src.unlink()
moved = True
except OSError:
pass
st[name] = {
"sub": entry.get("sub", ""),
"email": entry.get("email", ""),
"status": "discarded",
"ts": int(time.time()),
"probes": probes if probes else MAX_PROBES,
}
# 旁路账本:便于扫为何丢
try:
ledger = DISCARD_DIR / "_discarded.tsv"
with ledger.open("a", encoding="utf-8") as fh:
fh.write(
f"{int(time.time())}\t{name}\t{entry.get('email','')}\t"
f"{status}\t{reason or status}\n"
)
os.chmod(ledger, 0o600)
except OSError:
pass
log(
f" 🗑 discard {name} [{status}]"
f"{' → _discarded/' if moved else ' (no json)'}"
f" email={entry.get('email','')}"
)
return True
def sweep_discard(st: dict) -> int:
"""扫 state + cpa_ready丢掉所有真坏号 json。返回丢弃个数。"""
n = 0
for name in list(st.keys()):
if not RUN:
break
v = st[name]
if v.get("status") == "discarded":
discard_bad(name, st) # 清残留 json
continue
# 软 403 已用完延迟回测次数 → 升格 chat_dead 再丢
if v.get("status") in SOFT_RETEST_STATUSES:
if _retests_done(v) >= RETEST_MAX and _age_sec(v) >= RETEST_AFTER_SEC:
v["status"] = "chat_dead"
st[name] = v
if should_discard(v.get("status", ""), _probes(v)):
if discard_bad(name, st):
n += 1
write_state(st)
return n
def rescue_soft_discards(st: dict) -> int:
"""
把误丢的 403/probe_cap 从 _discarded 捞回主目录,标 chat_denied
立刻可做一次延迟回测ts 回拨到已到期)。
账本原因为 soft或 state 已 discarded 但 json 仍在 _discarded 且无硬坏标记。
"""
ensure_dir(DISCARD_DIR)
ensure_dir(OUT_DIR)
soft_reasons = SOFT_RETEST_STATUSES | {"probe_cap", "chat_denied", "forbidden_domain_ban"}
soft_names: set[str] = set()
ledger = DISCARD_DIR / "_discarded.tsv"
if ledger.exists():
for ln in ledger.read_text(encoding="utf-8").splitlines():
parts = ln.split("\t")
if len(parts) >= 5:
name, prev_st, reason = parts[1], parts[3], parts[4]
if prev_st in soft_reasons or reason in soft_reasons:
soft_names.add(name)
elif len(parts) >= 4:
name, prev_st = parts[1], parts[3]
if prev_st in soft_reasons:
soft_names.add(name)
n = 0
for p in sorted(DISCARD_DIR.glob("xai-*.json")):
if not RUN:
break
name = p.name
# 账本标 soft或 state 缺/已 discarded 时默认按 soft 捞(旧误丢)
entry = st.get(name) or {}
prev = entry.get("status", "discarded")
if name not in soft_names and prev not in ("discarded", "") and prev not in soft_reasons:
continue
if name not in soft_names and prev == "discarded":
# 无账本线索:仍捞(用户确认 403 常误杀);真 429 已不在 soft 集合时靠账本
soft_names.add(name)
if name not in soft_names:
continue
dest = OUT_DIR / name
try:
if dest.exists():
dest.unlink()
os.replace(p, dest)
os.chmod(dest, 0o600)
except OSError as exc:
log(f" ✗ rescue 移动失败 {name}: {exc}")
continue
# ts 回拨:立刻进入可回测窗口(不额外空等 RETEST_AFTER_SEC
st[name] = {
"sub": entry.get("sub", ""),
"email": entry.get("email", ""),
"status": "chat_denied",
"ts": int(time.time()) - RETEST_AFTER_SEC,
"probes": MAX_PROBES, # 留给 1 次回测额度(与当前 MAX_PROBES 对齐)
}
n += 1
log(f" ↩ rescue {name} → chat_denied (retest due) probes={MAX_PROBES}")
if n:
write_state(st)
return n
# ---------------------------------------------------------------------------
# main loop
# ---------------------------------------------------------------------------
def _probes(entry: dict) -> int:
try:
return int(entry.get("probes") or 0)
except (TypeError, ValueError):
return 0
def _age_sec(entry: dict) -> int:
try:
return max(0, int(time.time()) - int(entry.get("ts") or 0))
except (TypeError, ValueError):
return RETEST_AFTER_SEC
def _retests_done(entry: dict) -> int:
"""首探预算用 MAX_PROBES超出部分算延迟回测次数。"""
return max(0, _probes(entry) - MAX_PROBES)
def should_probe(name: str, st: dict) -> bool:
"""
新号首探≤MAX_PROBES 次当场连探)。
403 软状态:满首探后等 RETEST_AFTER_SEC再给 RETEST_MAX 次回测。
429/真坏/终态:不探。
"""
if name not in st:
return True
cur = st[name].get("status", "")
if cur == "discarded" or cur in TERMINAL_STATUSES:
return False
probes = _probes(st[name])
if cur in SOFT_RETEST_STATUSES:
if _retests_done(st[name]) >= RETEST_MAX:
return False
# 首探未打满(异常中断)→ 先补完首探,不算延迟回测
if probes < MAX_PROBES:
return True
return _age_sec(st[name]) >= RETEST_AFTER_SEC
# 新号 / pending / 临时错误:未满首探预算可探
if probes < MAX_PROBES and cur in ("", "pending", "probe_err", "unknown"):
return True
return False
def process_file(name: str, st: dict, *, force_probe: bool = False) -> None:
src_path = AUTH_DIR / name
# 回测/整备也可只读 cpa_ready 已有 jsonrescue 后 AUTH 可能仍在)
out_existing = OUT_DIR / name
if not src_path.exists() and not out_existing.exists():
return
src = None
if src_path.exists():
src = load_source(src_path)
if (not src or not src.get("access_token")) and out_existing.exists():
src = load_source(out_existing)
if not src:
return
if not src.get("access_token"):
log(f"{name} 无 access_token, 跳过")
return
if name in st and st[name].get("status") == "discarded":
return
sub, entry = finalize(src)
out_path = write_out(name, entry)
already = _probes(st[name]) if name in st else 0
prev_status = st[name].get("status", "") if name in st else ""
is_soft_retest = (
prev_status in SOFT_RETEST_STATUSES
and already >= MAX_PROBES
and _retests_done(st.get(name, {})) < RETEST_MAX
)
if not force_probe and not should_probe(name, st):
if name in st and should_discard(st[name].get("status", ""), already):
discard_bad(name, st)
write_state(st)
return
# 软状态未到点:静默整备
if prev_status in SOFT_RETEST_STATUSES:
left = max(0, RETEST_AFTER_SEC - _age_sec(st[name]))
if already >= MAX_PROBES and _retests_done(st[name]) < RETEST_MAX:
log(
f"{name} 403 待回测 "
f"({left}s / retest={_retests_done(st[name])}/{RETEST_MAX})"
)
return
log(
f"{name} 整备覆盖 "
f"({prev_status or '-'}/probes={already}) → {out_path.name}"
)
return
if force_probe and already >= MAX_PROBES + RETEST_MAX and not os.environ.get("ACPA_FORCE_IGNORE_CAP"):
log(f"{name} 已 probes={already},跳过强制重探(省额度)")
if should_discard(st.get(name, {}).get("status", ""), already):
discard_bad(name, st)
write_state(st)
return
if is_soft_retest:
budget = 1
phase = f"403回测×1 (after {RETEST_AFTER_SEC}s, done={_retests_done(st[name])}/{RETEST_MAX})"
else:
budget = max(0, MAX_PROBES - already)
phase = f"首探×≤{budget} (used={already}/{MAX_PROBES})"
# 新号首探前 warmup刚 mint 的 access 常被上游瞬时 403
if not is_soft_retest and PROBE_WARMUP_SEC > 0:
time.sleep(PROBE_WARMUP_SEC)
# 新号首探前 warmup刚 mint 的 access 常被上游瞬时 403
if not is_soft_retest and PROBE_WARMUP_SEC > 0:
time.sleep(PROBE_WARMUP_SEC)
log(f"{name} 整备完成, {phase} ...")
r, used = probe_budgeted(out_path, budget)
status = r.get("status", "probe_err")
snip = r.get("snippet", "")
new_probes = already + used
# 延迟回测仍 403 → 真坏 chat_dead
if is_soft_retest and status in (
"chat_denied", "forbidden_domain_ban", "probe_cap", "probe_err", "unknown",
):
status = "chat_dead"
log(
f" {name}: [chat_dead] 回测仍 403/失败 http={r.get('http')} "
f"probes={new_probes} {snip[:70]}"
)
else:
log(
f" {name}: [{status}] http={r.get('http')} "
f"probes={new_probes} {snip[:70]}"
)
st[name] = {
"sub": sub,
"email": entry.get("email", ""),
"status": status,
"ts": int(time.time()),
"probes": new_probes,
}
if should_discard(status, new_probes):
discard_bad(name, st)
write_state(st)
def scan_once(st: dict) -> int:
"""扫一轮新号首探403 到期回测429 等真坏丢弃alive 只刷整备。"""
if not AUTH_DIR.exists():
log(f"⚠ 出货目录不存在: {AUTH_DIR} (auth-service.sh 启动后才会建)")
# 仍处理 cpa_ready 里待回测/rescue 的号
pruned = prune_orphan_state(st)
if pruned:
log(f"本轮 prune 幽灵 state {pruned}")
discarded = sweep_discard(st)
if discarded:
log(f"本轮丢弃坏号 {discarded} 个 → {DISCARD_DIR.name}/")
names: set[str] = set()
if AUTH_DIR.exists():
names.update(p.name for p in AUTH_DIR.glob("xai-*.json"))
# cpa_ready 里所有现存 json 都纳入扫描:
# - soft 到期回测
# - 无 state删 TSV 后残留 / 手工丢入)→ 当新号首探
# - alive 走 should_probe=False 分支只刷整备
if OUT_DIR.exists():
names.update(p.name for p in OUT_DIR.glob("xai-*.json"))
n = 0
for name in sorted(names):
if not RUN:
break
if name in st and st[name].get("status") == "discarded":
continue
if name in st and not should_probe(name, st):
if should_discard(st[name].get("status", ""), _probes(st[name])):
discard_bad(name, st)
write_state(st)
continue
src_path = AUTH_DIR / name if (AUTH_DIR / name).exists() else OUT_DIR / name
src = load_source(src_path) if src_path.exists() else None
if src and src.get("access_token"):
_, entry = finalize(src)
write_out(name, entry)
continue
process_file(name, st)
n += 1
return n
def main() -> int:
ap = argparse.ArgumentParser(description="auto CPA 整备驻守")
ap.add_argument("--interval", type=float, default=2.0)
ap.add_argument("--once", action="store_true", help="扫一轮就退")
ap.add_argument(
"--reprobe-soft",
action="store_true",
help="立刻对 chat_denied/probe_cap 等软 403 做回测(仍 1 次/号,受 RETEST_MAX",
)
ap.add_argument(
"--no-rescue",
action="store_true",
help="启动时不从 _discarded 捞回误丢的 403",
)
args = ap.parse_args()
ensure_dir(OUT_DIR)
ensure_dir(DISCARD_DIR)
st = load_state()
n_prune = prune_orphan_state(st)
# 历史 probe_cap 并入 soft可走延迟回测
for name, v in st.items():
if v.get("status") == "probe_cap":
v["status"] = "chat_denied"
soft_n = sum(1 for v in st.values() if v.get("status") in SOFT_RETEST_STATUSES)
bad_n = sum(
1 for v in st.values()
if v.get("status") != "discarded"
and should_discard(v.get("status", ""), _probes(v))
)
log(
f"启动 | 出货={AUTH_DIR} | 落盘={OUT_DIR} | state={len(st)}"
f"(启动 prune {n_prune}) | 首探≤{MAX_PROBES}/号 "
f"| warmup={PROBE_WARMUP_SEC}s 403即重试={IMMEDIATE_403_RETRIES}×{IMMEDIATE_403_SLEEP}s "
f"| 403回测 after={RETEST_AFTER_SEC}s ×{RETEST_MAX} "
f"| soft_403={soft_n} | pending_discard={bad_n}"
)
if not args.no_rescue:
n_res = rescue_soft_discards(st)
if n_res:
log(f"启动捞回误丢 403 {n_res} 个(将回测)")
n0 = sweep_discard(st)
if n0:
log(f"启动丢弃真坏号 {n0} 个 → {DISCARD_DIR}/")
if st and not args.reprobe_soft:
log(
f"(state 已有: 刷 alive 整备403 满 {RETEST_AFTER_SEC}s 回测×{RETEST_MAX}"
f"429 free_exhausted 立刻丢)"
)
for name in list(st.keys()):
if not RUN:
break
if st[name].get("status") != "alive":
continue
src_path = AUTH_DIR / name
if src_path.exists():
src = load_source(src_path)
if src and src.get("access_token"):
_, entry = finalize(src)
write_out(name, entry)
if args.reprobe_soft:
# soft 立刻回测一次:拨到期 + 把 probes 收成 MAX_PROBES
# 避免旧版 probes=2 在新默认 MAX_PROBES=1 下被算成 retest 已用尽。
# 只动 softalive 绝不复探(省 free 额度)。
now = int(time.time())
targets = []
for name, v in st.items():
if v.get("status") in SOFT_RETEST_STATUSES:
v["ts"] = now - RETEST_AFTER_SEC
v["probes"] = MAX_PROBES
targets.append(name)
write_state(st)
log(f"--reprobe-soft: {len(targets)} 个 403 软状态立刻回测×1alive 不动)")
for name in targets:
if not RUN:
break
process_file(name, st, force_probe=True)
write_state(st)
alive = sum(1 for v in st.values() if v.get("status") == "alive")
dead = sum(
1 for v in st.values()
if v.get("status") in DISCARD_STATUSES or v.get("status") == "discarded"
)
soft = sum(1 for v in st.values() if v.get("status") in SOFT_RETEST_STATUSES)
log(f"回测结束 | alive={alive} soft_403={soft} dead/discard={dead} state={len(st)}")
return 0
while RUN:
try:
n = scan_once(st)
if n:
log(f"本轮处理 {n} 个号(新号/回测)")
except Exception as exc:
log(f"轮询异常: {type(exc).__name__} {exc}")
if args.once:
break
slept = 0.0
while slept < args.interval and RUN:
time.sleep(0.25)
slept += 0.25
log(f"退出 | state={len(st)} 条 写 {STATE_FILE}")
write_state(st)
return 0
if __name__ == "__main__":
sys.exit(main())

188
keys/sync_acc.py Executable file
View File

@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""
sync_acc.py — 把 keys/cpa_ready/ 里「探活通过」号的 access_token 同步进 keys/acc.md
默认只入库 acpa_watchdog 记为 alive 的号(读 keys/cpa_ready/_state.tsv
403 forbidden / token_dead / probe_err 等不进 acc.md避免坏号灌库。
用法:
python3 sync_acc.py # 常驻轮询(默认 5s), Ctrl-C 退
python3 sync_acc.py --once # 同步一次就退
python3 sync_acc.py --all # 忽略探活状态,全量抽 token排障用
python3 sync_acc.py --interval 10
acc.md 每轮全量重写(去重)0600。
"""
from __future__ import annotations
import argparse
import glob
import json
import os
import signal
import sys
import time
from pathlib import Path
PROJECT = Path(__file__).resolve().parent.parent # grok-free-register/
SRC_DIR = PROJECT / "keys" / "cpa_ready"
STATE_FILE = SRC_DIR / "_state.tsv"
OUT_FILE = PROJECT / "keys" / "acc.md"
# 默认可入库的探活状态discarded/坏号不在 cpa_ready 主目录,也不在此集合)
ALIVE_STATUSES = frozenset({"alive"})
SKIP_STATUSES = frozenset({
"discarded", "free_exhausted", "rate_limited", "token_dead",
"no_quota_paid_end", "probe_cap", "chat_denied", "chat_dead",
"forbidden_domain_ban", "probe_err", "unknown",
})
RUN = True
def _sig(_s, _f):
global RUN
RUN = False
signal.signal(signal.SIGINT, _sig)
signal.signal(signal.SIGTERM, _sig)
def log(msg: str) -> None:
print(f"[sync_acc {time.strftime('%H:%M:%S')}] {msg}", flush=True)
def load_alive_names() -> set[str] | None:
"""
读 _state.tsv → 状态为 alive 且盘上仍有 json 的文件名集合。
无 state 文件返回 None。幽灵 aliveTSV 有、json 无)不计。
"""
if not STATE_FILE.exists():
return None
alive: set[str] = set()
for ln in STATE_FILE.read_text(encoding="utf-8").splitlines():
parts = ln.split("\t")
if len(parts) < 4:
continue
name, status = parts[0], parts[3]
if status in ALIVE_STATUSES:
# 只认盘上真实存在的号,避免删库后 state 幽灵把计数钉死
if (SRC_DIR / name).exists():
alive.add(name)
# discarded / 坏状态明确不进(双保险;主目录 json 也应已被 watchdog 挪走)
elif status in SKIP_STATUSES:
continue
return alive
def collect(*, require_alive: bool) -> tuple[list[str], dict[str, int]]:
"""
返回 (去重 token 列表, 计数)。
require_alive=True 时只收 _state.tsv 标为 alive 且文件仍在的 xai-*.json。
"""
files = sorted(glob.glob(str(SRC_DIR / "xai-*.json")))
alive_names = load_alive_names() if require_alive else None
seen: set[str] = set()
out: list[str] = []
stats = {
"files": len(files),
"alive_state": len(alive_names) if alive_names is not None else -1,
"skipped_not_alive": 0,
"no_tok": 0,
"bad": 0,
"no_state_skip_all": 0,
}
if require_alive and alive_names is None:
# 有 cpa json 但还没 state宁可不灌避免 403 进库
if files:
log(f"⚠ 无 {STATE_FILE.name},拒绝全量入库(等 watchdog 探活)")
stats["no_state_skip_all"] = len(files)
return [], stats
for f in files:
name = Path(f).name
if require_alive and alive_names is not None and name not in alive_names:
stats["skipped_not_alive"] += 1
continue
try:
d = json.loads(Path(f).read_text(encoding="utf-8"))
except Exception:
stats["bad"] += 1
continue
tok = d.get("access_token")
if not isinstance(tok, str) or not tok:
stats["no_tok"] += 1
continue
if tok in seen:
continue
seen.add(tok)
out.append(tok)
return out, stats
def write_acc(tokens: list[str]) -> None:
OUT_FILE.parent.mkdir(parents=True, exist_ok=True)
# 固定同目录临时文件,避免 with_suffix 边界情况 + 保证 replace 原子
tmp = OUT_FILE.parent / (OUT_FILE.name + ".tmp")
tmp.write_text("\n".join(tokens) + ("\n" if tokens else ""), encoding="utf-8")
os.replace(tmp, OUT_FILE)
try:
os.chmod(OUT_FILE, 0o600)
except OSError:
pass
def sync_once(*, require_alive: bool) -> int:
toks, st = collect(require_alive=require_alive)
write_acc(toks)
mode = "alive-only" if require_alive else "all"
log(
f"sync → {OUT_FILE.name}: {len(toks)} tokens [{mode}] "
f"(files={st['files']} state_alive={st['alive_state']} "
f"skip_dead={st['skipped_not_alive']} no_tok={st['no_tok']} bad={st['bad']})"
)
return len(toks)
def main() -> int:
ap = argparse.ArgumentParser(
description="cpa_ready access_token → keys/acc.md (default: alive only)"
)
ap.add_argument("--interval", type=float, default=5.0)
ap.add_argument("--once", action="store_true", help="刷一次就退")
ap.add_argument(
"--all",
action="store_true",
help="忽略探活,全量抽 token排障会把 403 也写入 acc.md",
)
args = ap.parse_args()
require_alive = not args.all
if not SRC_DIR.exists():
log(f"⚠ 目录不存在: {SRC_DIR} (watchdog 整备后才会建)")
if args.once:
return 0
sync_once(require_alive=require_alive)
if args.once:
return 0
log(f"常驻轮询 interval={args.interval}s alive_only={require_alive} (Ctrl-C 退)")
while RUN:
slept = 0.0
while slept < args.interval and RUN:
time.sleep(0.25)
slept += 0.25
if not RUN:
break
sync_once(require_alive=require_alive)
log("退出")
return 0
if __name__ == "__main__":
sys.exit(main())